欢迎来到我们关于可视化Node.js
事件循环的系列文章的第六篇。在上一篇文章中,我们探索了I/O
轮询阶段,并简要了解了队列检查以及如何使用内置的setImmediate()
函数将函数排入队列。
在本文中,我们将进行更多的实验,以进一步理解队列检查。
前九个实验涵盖了微任务、计时器、
I/O
和队列检查,并在以前的文章中讨论过。所有实验都是使用CommonJS
模块格式运行的。
实验 10
1 | // index.js |
代码片段延续了前一个实验。它包括对readFile()
的调用,将回调函数排入I/O
队列。此外,还调用了process.nextTick()
,将回调函数排入nextTick
队列,调用了Promise.resolve().then()
,将回调函数排入promise
队列,以及调用了setTimeout()
,将回调函数排入计时器队列。
与前一个实验的不同之处在于setImmediate()
语句的执行。现在,它不是在最后执行,而是在readFile()
回调内部执行。这是为了确保只有在I/O
轮询完成后才排队setImmediate()
回调。
在执行完调用栈上的所有语句后,我们在nextTick
队列中有一个回调函数,在promise
队列中有另一个回调函数,在计时器队列中有一个回调函数。由于I/O
轮询尚未完成,I/O
队列中尚没有回调函数,因为在上一篇文章中我们了解到这一点。
当没有更多代码可执行时,控制权进入事件循环。首先,从nextTick
队列中出队并执行第一个回调函数,将一条消息记录到控制台。nextTick
队列为空后,事件循环继续到promise
队列。回调函数从队列中出队并在调用栈上执行,将一条消息记录到控制台。
此时,promise
队列为空,事件循环继续到计时器队列。在计时器队列中有一个回调函数出队并执行,导致控制台中打印出第三条消息。
现在,事件循环继续到I/O
队列,但在这一点上,该队列没有任何回调函数。然后进入I/O轮询阶段。在此阶段,来自已完成readFile()
操作的回调函数被推入I/O
队列。
事件循环然后进入检查队列和关闭队列,两者都为空。循环进行第二次迭代。它检查nextTick
队列、promise
队列、计时器队列,最终到达I/O
队列。
在这里,它遇到了一个新的回调函数,执行后导致第四条消息被记录到控制台。此回调还包括对setImmediate()
的调用,将另一个回调函数排入检查队列。
最后,事件循环继续到检查队列,出队回调函数并执行,导致最后一条消息被打印到控制台。
在微任务队列回调、计时器队列回调和I/O队列回调执行之后,检查队列回调被执行。
实验11
1 | // index.js |
让我们继续进行下一个实验,以更好地理解微任务队列和检查队列的优先级顺序。
在调用栈中的所有语句执行后,nextTick
队列、promise
队列和计时器队列中各有一个回调函数。由于I/O
轮询尚未完成,I/O
队列为空。当没有进一步的代码可执行时,事件循环开始。
事件循环按照以下顺序出队并执行回调函数:nextTick
、promise、计时器、I/O、检查和关闭。因此,第一个要执行的回调函数在nextTick队列中。
执行完后,事件循环继续到下一个队列,即promise
队列。接着执行promise
队列中的回调函数。
完成后,事件循环继续到计时器队列,在那里setTimeout()
回调被出队并执行。
然后,事件循环进入I/O
队列,该队列仍为空。它继续进入I/O
轮询阶段。完成readFile()
操作的回调函数被推入I/O
队列。
事件循环继续到检查队列和关闭队列,两者都为空。然后进入第二次迭代。在进入I/O
队列之前,检查了nextTick
、promise
和计时器队列,它们都为空。
然后,事件循环再次进入I/O
队列,遇到一个新的回调函数。第四条消息被记录到控制台。
回调函数包含对process.nextTick()
、Promise.resolve().then()
和setImmediate()
的调用,导致在nextTick
、Promise
和检查队列中排队了新的回调函数。
事实证明,在进入检查队列之前,事件循环会检查微任务队列。它在nextTick
队列中找到一个回调函数,执行它,并将相应的消息记录到控制台。然后检查promise
队列,执行回调函数,并将相应的消息记录到控制台。
最后,事件循环进入检查队列,出队回调函数并执行,导致最后一条消息被打印到控制台。
微任务队列的回调函数在
I/O
队列回调函数执行之后、检查队列回调函数之前被执行。
让我们继续微任务队列和检查队列的主题,进行我们的下一个实验。
实验 12
1 | 代码 |
代码包含三个对setImmediate()
函数的调用,每个调用都有相应的日志语句。然而,第二个setImmediate()
函数还包括对process.nextTick()
和Promise.resolve().then()
的调用。
当调用栈执行所有语句后,检查队列中有三个回调函数。
当没有更多的代码可执行时,控制权进入事件循环。由于没有回调函数,初始队列被跳过,重点在于检查队列。
第一个回调函数被出队并执行,导致第一个日志语句。接下来,第二个回调函数也被出队并执行,导致第二个日志语句。然而,第二个回调函数还在nextTick
队列和promise
队列中排队了回调函数。这些队列具有较高的优先级,并在回调函数执行之间进行检查。
因此,在检查队列中执行第二个回调函数后,nextTick
队列中的回调函数被出队并执行。接着执行promise
队列中的回调函数。
现在,当微任务队列为空时,控制权返回到检查队列,第三个回调函数被出队并执行。这将在控制台中打印出最后一条消息。
微任务队列的回调函数在检查队列的回调函数之间被执行。
在本文的最后一个实验中,我们将重新审视计时器队列的异常情况,并考虑检查队列。
实验 13
1 | // index.js |
代码中有一个调用setTimeout()
,延时为0ms,并紧接着一个调用setImmediate()
。
如果多次运行代码,你会注意到执行顺序不一致。
由于CPU
使用的不确定性,我们永远无法保证0ms
定时器和队列检查回调之间的执行顺序。有关更详细的解释,请参考实验7。
当使用0ms延迟运行setTimeout()和setImmediate()方法时,无法保证执行顺序。
结论
实验表明,在执行了微任务队列、计时器队列和I/O队列中的回调函数后,检查队列中的回调函数才会被执行。在检查队列回调之间,会执行微任务队列中的回调函数。当使用0ms延迟运行setTimeout()和setImmediate()方法时,执行顺序取决于CPU的工作负载。