nodejs事件循环

上周写了一篇关于require和module的文章,其中runMain函数中有一段代码这样的代码process._tickCallback();,不知道你们还记得不。这个代码其实和process.nextick有关系。

基础认知 Macrotask & Microtask

这里描述的知识点长话短说。
nodejs事件循环中,其实具有2种任务队列:Macrotask和Microtask。每次事件循环只会执行一个Macrotask队列中的任务,然后将Microtask队列中的所有任务执行完成。周而复始,直到结束。

  • Macrotask: setTimeout, setInterval, setImmediate, I/O, UI rendering
  • Microtask: process.nextTick, Promises, Object.observe, MutationObserver

    每一次事件循环都会按照:timers ->poll(I/O)->check(immediates)这样的顺序遍历。

    • timers阶段会执行所有达到延时的回调。
    • poll阶段执行所有获取数据的IO回调。
    • check阶段只执行一个最先设置的immediate回调。
    • timers阶段若设置了I/O任务,并且在poll阶段前数据就已经返回,该I/O回调将会在该次event loop的poll阶段执行
    • timers和poll阶段若设置了immediates的回调,并且此回调是最先设置的回调,则该回调会在该次event loop的check阶段执行。

例子:

1
2
3
4
5
6
7
8
9
10
11
setImmediate(function () {
console.log(1);
}, 0);
setTimeout(function () {
console.log(2);
process.nextTick(function () {
console.log(3);
});
}, 0);
//结果:2,3,1 其实也有可能是1,2,3 后面会解释

根据现在这个结果,可以看出Microtask类型的nextTick确实比Macrotask类型的setImmediate要先执行。

上面的结论真对吗

setImmediate vs setTimeout 谁先执行

上面的例子执行多次后,会发现setImmediate有时会比setTimeout先执行。原因是什么,timers不是应该在check immediate之前遍历执行吗。其实,就算setTimeout设置的是0毫秒,当轮询在timers阶段,是会去判断timer设置的延时是否已经到达,如果到达才会执行。只要轮询足够快,在timer的延迟到达之前执行timers阶段,就不会执行settimeout,所以自然就会出现,setImmediate在setTimeout之前执行的情况。官方文档是这样说的setImmediate vs setTimeout,虽然只是简单的说了受性能的限制。

process.nextTick vs Promises.then 谁先执行

其实process.nextTick是一直会在Promises之前执行的,为什么呢?我们先看下process._tickCallback();源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function _tickCallback() {
do {
while (tickInfo[kIndex] < tickInfo[kLength]) {
++tickInfo[kIndex];
const tock = nextTickQueue.shift();
const callback = tock.callback;
const args = tock.args;
emitBefore(tock[async_id_symbol], tock[trigger_async_id_symbol]);
if (async_hook_fields[kDestroy] > 0)
emitDestroy(tock[async_id_symbol]);
_combinedTickCallback(args, callback);
emitAfter(tock[async_id_symbol]);
if (kMaxCallbacksPerLoop < tickInfo[kIndex])
tickDone();
}
tickDone();
_runMicrotasks();
emitPendingUnhandledRejections();
} while (tickInfo[kLength] !== 0);
}

可以看出,所有的nextTick被存储在nextTickQueue队列中。每次循环取出一个tick,通过_combinedTickCallback函数执行nextTick。最后再执行_runMicrotasks函数(包括Promises的执行)。所以,process.nextTick永远优先于Promises执行。

Macrotask & Microtask 到底谁先执行

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

前面说过Macrotask比Microtask先执行,为什么这里Promise和nextTick比setTimeout先执行?其实,官方文档里面这里说的,可以将主进程作为一个Macrotask。但是,在源码Module._load(process.argv[1], null, true);process._tickCallback();里面可以看见,主进程执行最后执行了_tickCallback,触发了Microtask队列里的任务。如果上面这段代码在I/O里面执行,那遵循事件循环遍历顺序,就能找到原因。

性能问题

Microtask队列太长,造成Macrotask较长时间无法执行,最终引起I/O问题。通过上面_tickCallback方法的源码,我们知道nextTick设置了个最大值const kMaxCallbacksPerLoop = 1e4;,考虑了性能方面问题。

参考:
next_tick源码