Event Loop详解

1,068 阅读3分钟

浏览器Event Loop执行流程

事件循环其实就是入栈出栈的循环。上面例子中说到了setTimeout,那setInterval呢,Promise呢等等等等,有很多异步的函数。但是这些异步任务有分宏任务(macro-task)和微任务(micro-task): macro-task包括: setTimeout, setInterval, setImmediate, I/O, UI rendering。 micro-task包括:process.nextTick, Promises, Object.observe, MutationObserver。 每一次Event Loop触发时:

  1. 执行完主执行线程中的任务。
  2. 取出micro-task中任务执行直到清空。
  3. 取出macro-task中一个任务执行。
  4. 取出micro-task中任务执行直到清空。
  5. 重复3和4。

Node Event Loop执行流程

js执行为单线程,所有代码皆在主线程调用栈完成执行。当主线程任务清空后才会去轮询取任务队列中任务。

循环阶段

在node中事件每一轮循环按照顺序分为6个阶段,来自libuv的实现:

  1. timers:执行满足条件的setTimeout、setInterval回调。
  2. I/O callbacks:是否有已完成的I/O操作的回调函数,来自上一轮的poll残留。
  3. idle,prepare:可忽略
  4. poll:等待还没完成的I/O事件,会因timers和超时时间等结束等待。
  5. check:执行setImmediate的回调。
  6. close callbacks:关闭所有的closing handles,一些onclose事件。

几个队列

除上述循环阶段中的任务类型,我们还剩下浏览器和node共有的microtask和node独有的process.nextTick,我们称之为Microtask Queue和NextTick Queue。

我们把循环中的几个阶段的执行队列也分别称为Timers Queue、I/O Queue、Check Queue、Close Queue。

循环之前

在进入第一次循环之前,会先进行如下操作:

  • 同步任务
  • 发出异步请求
  • 规划定时器生效的时间
  • 执行process.nextTick()

开始循环

按照我们的循环的6个阶段依次执行,每次拿出当前阶段中的全部任务执行,清空NextTick Queue,清空Microtask Queue。再执行下一阶段,全部6个阶段执行完毕后,进入下轮循环。即:

  • 清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。
  • 清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。
  • 清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。
  • 清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。
  • 进入下轮循环。

可以看出,nextTick优先级比promise等microtask高。setTimeoutsetInterval优先级比setImmediate高。

注意

  • 如果在timers阶段执行时创建了setImmediate则会在此轮循环的check阶段执行,如果在timers阶段创建了setTimeout,由于timers已取出完毕,则会进入下轮循环,check阶段创建timers任务同理。
  • setTimeout优先级比setImmediate高,但是由于setTimeout(fn,0)的真正延迟不可能完全为0秒,可能出现先创建的setTimeout(fn,0)而比setImmediate的回调后执行的情况。

伪代码

while (true) {
  loop.forEach((阶段) => {
    阶段全部任务()
    nextTick全部任务()
    microTask全部任务()
  })
  loop = loop.next
}

实际例子

setImmediate(function(){
    console.log(1);
},0);
setTimeout(function(){
    console.log(2);
},0);
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});
console.log(6);
process.nextTick(function(){
    console.log(7);
});
console.log(8);
结果是:3 4 6 8 7 5 2 1

优先级关系如下:

process.nextTick > promise.then > setTimeout > setImmediate

V8实现中,两个队列各包含不同的任务:

macrotasks: script(整体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promises, Object.observe, MutationObserver

参考

  1. https://juejin.cn/post/6844903574560833550