理解Node.js中Event Loop(六)

理解Node.js中Event Loop(六)

本文翻译至:Visualizing the Check Queue in the Node.js Event Loop

欢迎阅读我们关于可视化 Node.js 事件循环系列的第六篇文章。在上一篇文章中,我们探讨了 I/O 轮询阶段,并简要了解了检查队列以及如何使用内置 setImmediate() 函数将函数入队列。

在本文中,我们将进行更多实验来进一步了解Check(检查)队列。

目录

实验10

index.js
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

index.js
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 队列将为空。当没有更多代码要执行时,事件循环开始。

  1. 事件循环按以下顺序出列并执行回调: nextTick 、promise、Timer、I/O、Check和Close。因此,第一个执行的回调位于 nextTick 队列中。

  2. 一旦执行,事件循环就会进入下一个队列,即 Promise 队列。接下来执行承诺队列回调。

  3. 完成后,事件循环进入Timer队列, setTimeout() 回调从队列中出列并执行。

  4. 然后事件循环进入 I/O 队列,该队列仍然是空的。它进入 I/O 轮询阶段。已完成的 readFile() 操作的回调函数被推入I/O队列。

  5. 事件循环进入Check队列和Close队列,这两个队列都是空的。然后它进入第二个周期。 nextTick 、 Promise 和 Timer 队列在 I/O 队列之前被检查,当然它们都是空的。

  6. 然后事件循环再次进入 I/O 队列,在那里遇到新的回调函数。第四条消息记录到控制台。

  7. 回调函数包含对 process.nextTick()Promise.resolve().then()setImmediate() 的调用,导致新的回调函数在 nextTick、Promise 和 check 队列中排队。

事实证明,在进入Check队列之前,事件循环会检查微任务队列。它在 nextTick 队列中找到回调,执行它,并将相应的消息记录到控制台。然后它检查 Promise 队列,执行回调,并将相应的消息记录到控制台。

微任务队列的"微"就体现在这了,与Timer队列, Check队列, I/O队列,Close队列相比。

  1. 最后,事件循环进入Check队列,使回调函数出队并执行它,从而将最后一条消息打印到控制台。

由实验11推断

微任务队列回调在 I/O 队列回调之后、检查队列回调之前执行。

让我们继续下一个实验的微任务队列和检查队列的主题。

实验12

index.js
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个回调。

  1. 控制进入事件循环。由于前几个队列没有回调,因此会跳过为空的队列,直到Check队列。

  2. 第一个回调出队并执行,产生第一个日志打印。接下来,第二个回调也出列并执行,从而产生第二条日志打印。但是,第二个回调还会在 nextTick 队列Promise 队列中插入回调。这些队列具有高优先级,并在回调执行之间进行检查。

  3. 这样,在执行检查队列中的第二个回调后, nextTick 队列回调就会出队并执行。接下来是 promise队列回调被出队并执行。

  4. 现在,当微任务队列为空时,控制权返回到检查队列,第三个回调将出队并执行。这将在控制台中打印最终消息。

由实验12推断

微任务队列回调 在 检查队列回调 之间执行。

对于本文的最后一个实验,我们将重新审视Timer队列异常,同时考虑检查队列。

实验13

index.js
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的工作负载。


此系列文章:

理解Node.js中Event Loop(一)

理解Node.js中Event Loop(二)

理解Node.js中Event Loop(三)

理解Node.js中Event Loop(四)

理解Node.js中Event Loop(五)

理解Node.js中Event Loop(六)

理解Node.js中Event Loop(七)