【六】Node.js事件循环中的队列检查


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

在本文中,我们将进行更多的实验,以进一步理解队列检查。

前九个实验涵盖了微任务、计时器、I/O和队列检查,并在以前的文章中讨论过。所有实验都是使用CommonJS模块格式运行的。

实验 10

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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(),将回调函数排入计时器队列。

与前一个实验的不同之处在于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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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队列和计时器队列中各有一个回调函数。由于I/O轮询尚未完成,I/O队列为空。当没有进一步的代码可执行时,事件循环开始。

事件循环按照以下顺序出队并执行回调函数:nextTick、promise、计时器、I/O、检查和关闭。因此,第一个要执行的回调函数在nextTick队列中。

执行完后,事件循环继续到下一个队列,即promise队列。接着执行promise队列中的回调函数。

完成后,事件循环继续到计时器队列,在那里setTimeout()回调被出队并执行。

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

事件循环继续到检查队列和关闭队列,两者都为空。然后进入第二次迭代。在进入I/O队列之前,检查了nextTickpromise和计时器队列,它们都为空。

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

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

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

最后,事件循环进入检查队列,出队回调函数并执行,导致最后一条消息被打印到控制台。

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

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

实验 12

1
2
3
4
5
6
7
8
9
代码
//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()的调用。

当调用栈执行所有语句后,检查队列中有三个回调函数。

当没有更多的代码可执行时,控制权进入事件循环。由于没有回调函数,初始队列被跳过,重点在于检查队列。

第一个回调函数被出队并执行,导致第一个日志语句。接下来,第二个回调函数也被出队并执行,导致第二个日志语句。然而,第二个回调函数还在nextTick队列和promise队列中排队了回调函数。这些队列具有较高的优先级,并在回调函数执行之间进行检查。

因此,在检查队列中执行第二个回调函数后,nextTick队列中的回调函数被出队并执行。接着执行promise队列中的回调函数。

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

微任务队列的回调函数在检查队列的回调函数之间被执行。

在本文的最后一个实验中,我们将重新审视计时器队列的异常情况,并考虑检查队列。

实验 13

1
2
3
// index.js
setTimeout(() => console.log("this is setTimeout 1"), 0);
setImmediate(() => console.log("this is setImmediate 1"));

代码中有一个调用setTimeout(),延时为0ms,并紧接着一个调用setImmediate()

如果多次运行代码,你会注意到执行顺序不一致。

由于CPU使用的不确定性,我们永远无法保证0ms定时器和队列检查回调之间的执行顺序。有关更详细的解释,请参考实验7。

当使用0ms延迟运行setTimeout()和setImmediate()方法时,无法保证执行顺序。

结论

实验表明,在执行了微任务队列、计时器队列和I/O队列中的回调函数后,检查队列中的回调函数才会被执行。在检查队列回调之间,会执行微任务队列中的回调函数。当使用0ms延迟运行setTimeout()和setImmediate()方法时,执行顺序取决于CPU的工作负载。