004:如何理解EventLoop——宏任务和微任务篇
宏任务(MacroTask)引入
在 JS 中,大部分的任务都是在主线程上执行,常见的任务有:
- 渲染事件
- 用户交互事件
- js脚本执行
- 网络请求、文件读写完成事件等等。
为了让这些事件有条不紊地进行,JS引擎需要对之执行的顺序做一定的安排,V8 其实采用的是一种队列
的方式来存储这些任务,
即先进来的先执行。模拟如下:
bool keep_running = true;
void MainTherad(){
for(;;){
//执行队列中的任务
Task task = task_queue.takeTask();
ProcessTask(task);
//执行延迟队列中的任务
ProcessDelayTask()
if(!keep_running) //如果设置了退出标志,那么直接退出线程循环
break;
}
}
这里用到了一个 for 循环,将队列中的任务一一取出,然后执行,这个很好理解。但是其中包含了两种任务队列,除了上述提到的任务队列, 还有一个延迟队列,它专门处理诸如setTimeout/setInterval这样的定时器回调任务。
上述提到的,普通任务队列和延迟队列中的任务,都属于宏任务。
微任务(MicroTask)引入
对于每个宏任务而言,其内部都有一个微任务队列。那为什么要引入微任务?微任务在什么时候执行呢?
其实引入微任务的初衷是为了解决异步回调的问题。想一想,对于异步回调的处理,有多少种方式?总结起来有两点:
- 将异步回调进行宏任务队列的入队操作。
- 将异步回调放到当前宏任务的末尾。
如果采用第一种方式,那么执行回调的时机应该是在前面所有的宏任务
完成之后,倘若现在的任务队列非常长,那么回调迟迟得不到执行,造成应用卡顿
。
为了规避这样的问题,V8 引入了第二种方式,这就是微任务
的解决方式。在每一个宏任务中定义一个微任务队列,当该宏任务执行完成,会检查其中的微任务队列,如果为空则直接执行下一个宏任务,如果不为空,则依次执行微任务
,执行完成才去执行下一个宏任务。
常见的微任务有MutationObserver、Promise.then(或.reject) 以及以 Promise 为基础开发的其他技术(比如fetch API), 还包括 V8 的垃圾回收过程。
Ok, 这便是宏任务
和微任务
的概念,接下来正式介绍JS非常重要的运行机制——EventLoop。