【二】Node.js事件循环中nextTick和Promise队列


欢迎来到系列文章中的第二篇,我们将继续以可视化方式探究 Node.js 事件循环。在第一篇文章中,我们了解到事件循环是 Node.js 中至关重要的一部分,帮助协调同步和异步代码的执行。

它由六个不同的队列组成。一个 nextTick 队列和一个 promise 队列(在本系列文章中称为微任务队列),一个定时器队列,一个 I/O 队列,一个检查队列,最后一个是关闭队列。

在每个循环中,当适当时,回调函数会从队列中出列并在调用堆栈上执行。在开始这篇文章之前,让我们先了解如何在这些队列中排队回调函数。

回调函数排队

要在 nextTick 队列中排队回调函数,我们使用内置的 process.nextTick() 方法。语法很简单:process.nextTick(callbackFn)。当此方法在调用堆栈上执行时,回调函数将被排队到 nextTick 队列中。

要在 promise 队列中排队回调函数,我们将使用 Promise.resolve().then(callbackFn)。当 promise 解决时,传递给 then() 块的函数将被排队到 promise 队列中。

现在我们知道如何向这两个队列中添加回调函数了,让我们从第一个实验开始。

所有实验都使用 CommonJS 模块格式进行。

实验 1
1
2
3
4
// index.js
console.log("console.log 1");
process.nextTick(() => console.log("this is process.nextTick 1"));
console.log("console.log 2");

这里有一段最小的代码片段,记录了三个不同的语句。第二个语句使用 process.nextTick() 方法将回调函数排队到 nextTick 队列中。

第一个 console.log()语句被推入调用堆栈并执行。它将相应的消息记录到控制台,然后从堆栈中弹出。

接下来,·process.nextTick()· 在调用堆栈上执行。这将回调函数排队到 nextTick 队列中,并被弹出。由于仍然有用户编写的代码要执行,回调函数必须等待执行。

执行继续,并且最后一个 console.log() 语句被推入堆栈。消息被记录到控制台,并将函数从堆栈中弹出。现在,没有更多用户编写的同步代码要执行了,因此控制权进入事件循环。

来自nextTick队列的回调函数被推到堆栈上,console.log() 也被推到堆栈上,执行并将相应的消息记录到控制台。

猜测
所有用户编写的同步 JavaScript 代码优先于运行时希望最终执行的异步代码。

让我们继续进行第二个实验。

实验 2
1
2
3
// index.js
Promise.resolve().then(() => console.log("this is Promise.resolve 1"));
process.nextTick(() => console.log("this is process.nextTick 1"));

我们调用了一次 Promise.resolve().then() 和一次 process.nextTick()

当调用堆栈执行第 1 行时,它将回调函数排入 promise 队列中。

当调用堆栈执行第 2 行时,它将回调函数排入 nextTick 队列中。

在第 2 行之后,没有更多的用户编写的代码要执行。

控制权进入事件循环,其中 nextTick 队列优先于 promise 队列(这是 Node.js 运行时的工作原理)。

事件循环执行 nextTick 队列回调函数,然后执行 promise 队列回调函数。

控制台显示 "this is process.nextTick 1",然后显示 "this is Promise.resolve 1"

猜测
在 nextTick 队列中的所有回调在 promise 队列中的所有回调之前执行。

让我为您详细解释上述第二个实验的更复杂版本。

额外实验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// index.js
process.nextTick(() => console.log("this is process.nextTick 1"));
process.nextTick(() => {
console.log("this is process.nextTick 2");
process.nextTick(() =>
console.log("this is the inner next tick inside next tick")
);
});
process.nextTick(() => console.log("this is process.nextTick 3"));

Promise.resolve().then(() => console.log("this is Promise.resolve 1"));
Promise.resolve().then(() => {
console.log("this is Promise.resolve 2");
process.nextTick(() =>
console.log("this is the inner next tick inside Promise then block")
);
});
Promise.resolve().then(() => console.log("this is Promise.resolve 3"));

代码包含三个对 process.nextTick() 的调用和三个对 Promise.resolve() 语句的调用。每个回调函数都记录了相应的消息。

当调用堆栈执行第1行时,它将回调函数排入 Promise 队列。

当调用堆栈执行第2行时,它将回调函数排入 nextTick 队列。

在第2行之后没有更多的用户编写的代码需要执行。

控制权进入事件循环,在此处 nextTick 队列优先于 promise 队列(这是 Node.js 运行时的工作原理)。

事件循环执行 nextTick 队列的回调函数,然后执行 promise 队列的回调函数。

控制台显示 “this is process.nextTick 1”,然后是 “this is Promise.resolve 1”。

猜测
所有 nextTick 队列中的回调函数在 promise 队列中的回调函数之前执行。

让我带您更详细地了解上述第二个实验的更复杂版本。

额外实验2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// index.js
process.nextTick(() => console.log("this is process.nextTick 1"));
process.nextTick(() => {
console.log("this is process.nextTick 2");
process.nextTick(() =>
console.log("this is the inner next tick inside next tick")
);
});
process.nextTick(() => console.log("this is process.nextTick 3"));

Promise.resolve().then(() => console.log("this is Promise.resolve 1"));
Promise.resolve().then(() => {
console.log("this is Promise.resolve 2");
process.nextTick(() =>
console.log("this is the inner next tick inside Promise then block")
);
});
Promise.resolve().then(() => console.log("this is Promise.resolve 3"));

该代码包含三个对 process.nextTick() 的调用和三个对 Promise.resolve() 语句的调用。每个回调函数记录了相应的消息。

然而,第二个 process.nextTick() 和第二个Promise.resolve()都有一个额外的process.nextTick()语句,每个都带有一个回调函数。

为了加快对此流程的解释,我将省略调用堆栈。当调用堆栈执行了所有六个语句时,nextTick 队列中有三个回调函数,而 promise 队列中有三个。在没有其他要执行的内容时,控制权进入事件循环。

如我们所知,nextTick 队列具有优先级。首先执行第一个回调函数,并将相应的消息记录到控制台。

接下来,执行第二个回调函数,该函数记录第二个日志语句。然而,此回调函数包含另一个 process.nextTick() 的调用,该调用将内部日志语句排入 nextTick 队列的末尾。

然后,Node 执行第三个 nextTick 回调,并将相应的消息记录到控制台。最初只有三个回调函数,但第二个回调函数添加了另一个回调函数到队列中,现在轮到了它。

事件循环推送内部 nextTick 回调,并执行 console.log() 语句。

nextTick 队列为空,控制流转到promise队列。promise 队列类似于 nextTick 队列。

首先记录 "Promise.resolve 1",然后是 "Promise.resolve 2"。但是,使用 process.nextTick() 将一个函数添加到 nextTick 队列中。尽管如此,控制仍然留在 promise 队列中,并继续执行其他回调函数。然后我们得到 Promise.resolve 3,此时 promise 队列为空。

Node 将再次检查微任务队列中是否有新的回调。由于 nextTick 队列中有一个回调,它会执行该回调,导致我们的最后一条日志语句。

这可能是一个稍微高级的实验,但推断仍然是相同的。

猜测
在所有回调函数执行之前,nextTick 队列中的所有回调函数都会执行。

在使用 process.nextTick() 时要小心。过度使用此方法可能会导致事件循环饥饿,从而阻止队列的其余部分运行。即使有大量的 nextTick() 调用,也可能会阻止I/O队列执行自己的回调。官方文档建议使用 process.nextTick() 有两个主要原因:处理错误或在调用堆栈展开之后但在事件循环继续之前运行回调。在使用 process.nextTick() 时,请务必谨慎使用。

额外实践

实验表明,所有用户编写的同步 JavaScript 代码优先于运行时希望最终执行的异步代码,并且在 nextTick 队列中的所有回调函数之前,会执行所有 promise 队列中的回调函数。