理解Node.js中Event Loop(六)
本文翻译至:Visualizing the Check Queue in the Node.js Event Loop
欢迎阅读我们关于可视化 Node.js 事件循环系列的第六篇文章。在上一篇文章中,我们探讨了 I/O 轮询阶段,并简要了解了检查队列以及如何使用内置 setImmediate()
函数将函数入队列。
在本文中,我们将进行更多实验来进一步了解Check(检查)队列。
目录
实验10
const fs = require("fs");
fs.readFile(__filename, () => {
console.log("this is readFile 1");
setImmediate(() => console.log("this is setImmediate 1"));
});
process.nextTick(() => console.log("this is process.nextTick 1"));
Promise.resolve().then(() => console.log("this is Promise.resolve 1"));
setTimeout(() => console.log("this is setTimeout 1"), 0);
for (let i = 0; i < 2000000000; i++) {}
该代码片段延续了之前的实验。它包括:
对
readFile()
的调用,该调用将回调函数放入 I/O 队列中对
process.nextTick()
的调用,它将回调函数排队到 nextTick 队列中调用
Promise.resolve().then()
,将回调函数排队到 Promise 队列中调用
setTimeout()
,将回调函数放入 Timer队列中
可视化:
执行完调用堆栈上的所有语句后,我们最终会:
在 nextTick 队列 中得到一个回调
在 Promise 队列 中得到另一个回调
在 Timer 队列 中得到一个回调。
I/O 队列中还没有回调,因为 I/O 轮询尚未完成,正如我们在上一篇文章中了解到的。
当没有更多代码要执行时,控制进入事件循环。 nextTick 队列中的第一个回调将出队并执行,并将消息打印到控制台。当 nextTick 队列为空时,事件循环将移至 Promise 队列。回调出队并在调用堆栈上执行,并在控制台中记录一条消息。
此时,promise 队列为空,事件循环进入Timer队列。Timer队列中有一个回调被出队并执行,使得控制台中出现第三条日志消息。
现在,事件循环继续到 I/O 队列,但此时该队列没有任何回调。然后进入 I/O 轮询阶段。在此阶段,已完成的readFile()
操作的回调函数被推入I/O队列。然后事件循环进入检查队列和关闭队列,这两个队列都是空的。循环继续进行第二次周期。它检查 nextTick 队列、promise队列、timer队列,最后到达I/O队列。
在这里,它遇到一个被添加的新回调函数,第四条消息被记录到控制台。此回调还包括对 setImmediate()` 的调用,此时它将另一个回调函数放入检查队列中。
最后,事件循环进入检查队列,使回调函数出队并执行它,从而在控制台中打印最后一条消息。
由实验10推断
检查队列回调在 微任务队列回调、定时器队列回调 和I/O队列回调 执行 后 执行。
实验11
const fs = require("fs");
fs.readFile(__filename, () => {
console.log("this is readFile 1");
setImmediate(() => console.log("this is setImmediate 1"));
process.nextTick(() =>
console.log("this is inner process.nextTick inside readFile")
);
Promise.resolve().then(() =>
console.log("this is inner Promise.resolve inside readFile")
);
});
process.nextTick(() => console.log("this is process.nextTick 1"));
Promise.resolve().then(() => console.log("this is Promise.resolve 1"));
setTimeout(() => console.log("this is setTimeout 1"), 0);
for (let i = 0; i < 2000000000; i++) {}
可视化
执行完调用堆栈中的所有语句后, nextTick 、 Promise 和 Timer 队列中的每一个都会有一个回调。由于 I/O 轮询尚未完成,I/O 队列将为空。当没有更多代码要执行时,事件循环开始。
事件循环按以下顺序出列并执行回调: nextTick 、promise、Timer、I/O、Check和Close。因此,第一个执行的回调位于 nextTick 队列中。
一旦执行,事件循环就会进入下一个队列,即 Promise 队列。接下来执行承诺队列回调。
完成后,事件循环进入Timer队列,
setTimeout()
回调从队列中出列并执行。然后事件循环进入 I/O 队列,该队列仍然是空的。它进入 I/O 轮询阶段。已完成的
readFile()
操作的回调函数被推入I/O队列。事件循环进入Check队列和Close队列,这两个队列都是空的。然后它进入第二个周期。 nextTick 、 Promise 和 Timer 队列在 I/O 队列之前被检查,当然它们都是空的。
然后事件循环再次进入 I/O 队列,在那里遇到新的回调函数。第四条消息记录到控制台。
回调函数包含对
process.nextTick()
、Promise.resolve().then()
和setImmediate()
的调用,导致新的回调函数在 nextTick、Promise 和 check 队列中排队。
微任务队列的"微"就体现在这了,与Timer队列, Check队列, I/O队列,Close队列相比。
- 最后,事件循环进入Check队列,使回调函数出队并执行它,从而将最后一条消息打印到控制台。
由实验11推断
微任务队列回调在 I/O 队列回调之后、检查队列回调之前执行。
让我们继续下一个实验的微任务队列和检查队列的主题。
实验12
setImmediate(() => console.log("this is setImmediate 1"));
setImmediate(() => {
console.log("this is setImmediate 2");
process.nextTick(() => console.log("this is process.nextTick 1"));
Promise.resolve().then(() => console.log("this is Promise.resolve 1"));
});
setImmediate(() => console.log("this is setImmediate 3"));
该代码包含对 setImmediate()
函数的三次调用,每次调用都有相应的日志语句。但是,第二次 setImmediate()
函数还包含对 process.nextTick()
和 Promise.resolve().then()
的调用。
调用栈执行完所有语句后,检查队列中有3个回调。
控制进入事件循环。由于前几个队列没有回调,因此会跳过为空的队列,直到Check队列。
第一个回调出队并执行,产生第一个日志打印。接下来,第二个回调也出列并执行,从而产生第二条日志打印。但是,第二个回调还会在 nextTick 队列和 Promise 队列中插入回调。这些队列具有高优先级,并在回调执行之间进行检查。
这样,在执行检查队列中的第二个回调后, nextTick 队列回调就会出队并执行。接下来是 promise队列回调被出队并执行。
现在,当微任务队列为空时,控制权返回到检查队列,第三个回调将出队并执行。这将在控制台中打印最终消息。
由实验12推断
微任务队列回调 在 检查队列回调 之间执行。
对于本文的最后一个实验,我们将重新审视Timer队列异常,同时考虑检查队列。
实验13
setTimeout(() => console.log("this is setTimeout 1"), 0);
setImmediate(() => console.log("this is setImmediate 1"));
一次 setTimeout()
的调用,延迟 0 毫秒,然后立即调用 setImmediate()
。
如果多次运行该代码,您会发现执行顺序不一致。
由于CPU使用率的不确定性,我们永远无法保证0ms定时器和检查队列回调之间的执行顺序。更深入的解释请参考实验7。
由实验13推断
当延迟 0ms 运行 setTimeout() 和 setImmediate() 方法时,执行顺序永远无法保证。
结论
实验表明,检查队列中的回调是在微任务队列、Timer队列 和 I/O队列中的回调执行完之后执行的。在检查队列回调之间,执行微任务队列回调。当以0ms
延迟运行 setTimeout()
和 setImmediate()
方法时,执行顺序取决于CPU的工作负载。
此系列文章: