本文翻译于 Visualizing nextTick and Promise Queues in Node.js Event Loop
这是解析
Node.js事件循环系列文章的第二篇。在第一篇文章中我们了解到事件循环是Node.js的重要组成部分,协助Node.js执行同步代码和异步代码。事件循环拥有六个不同的队列,一个
nextTick队列和一个promise队列(其中这两个队列被称为微任务队列microtask queues),一个timer队列,一个I/O队列,一个check队列和一个close队列。在这个循环中,回调函数会在适当的时候出队,并在堆栈上执行。在本文中,让我们执行几个事例来检验我们的可视化事件循环是正确的。
我们第这部分的事例主要关注 nextTick 队列和 promise 队列。在执行事例之前,我们先来了解一下是如何把回调函数放入这些队列的。
把回调函数加入 nextTick 队列,我们是通过调用内置的 process.nextTick() 方法。语法很简单:process.nextTick(callbackFn)。当我们在执行此方法时,回调函数将加入 nextTick 队列。
把回调函数加入 Promise 队列,我们是通过调用 Promise.resolve().then(callbackFn)。当 Promise 解析时,传递给 then() 中的回调函数将在 promise 队列中排队。
现在我们了解了如何向两个队列添加回调函数,让我们开始第一个事例。
事例1
// index.js
console.log("console.log 1");
process.nextTick(() => console.log("this is process.nextTick 1"));
console.log("console.log 2");
我们写了一个代码片段,用于打印三个不同的语句。第二个语句使用 process.nextTick() 方法将回调函数放入nextTick队列中。
- 第 1 行
console.log()语句被推送到调用堆栈来执行。它在控制台中打印相应的消息,然后从堆栈中弹出。 - 接下来,
process.nextTick()在调用堆栈上执行。这会将回调函数放入到nextTick队列中并弹出。由于仍有用户编写的代码要执行,因此回调函数必须等待直到下次轮到它。 - 执行继续,最后一行
console.log()语句被推送到堆栈上。消息被打印到控制台,函数从堆栈中弹出。现在,没有更多用户编写的同步代码要执行,因此控制权进入事件循环。 - 来自
nextTick队列的回调函数被推送到堆栈,console.log()被推送到堆栈上执行,并且相应的消息被打印到控制台。
结论
所有用户编写的同步 JavaScript 代码都优先于异步代码执行
事例2
// 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 队列中的回调函数之前执行
事例3
// 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队列末尾。 -
然后执行第三个
nextTick回调,将相应消息打印到控制台。最初只有三个回调,但第二个回调将另一个回调添加到队列中,现在轮到它了。 -
事件循环执行最后一个
nextTick回调,打印this is the inner next tick inside next tick到控制台。 -
nextTick队列为空,控制权将转到promise队列。promise队列的执行过程与nextTick队列类似。 -
首先,打印了
Promise.resolve 1,然后打印Promise.resolve 2。这时调用了process.nextTick(),将一个函数被添加到nextTick队列中。但是控制权仍然在promise队列,会继续执行promise队列中的其他回调函数。然后打印Promise.resolve 3,此时,promise队列为空。 -
Node.js将再次检查微任务队列中是否有新的回调。这时发现nextTick队列中还有一个回调函数,它会执行该回调函数,打印最后一条日志语句。
这是一个稍微复杂一点的事例,但结论与上面的事例是一样的。
结论
nextTick 队列中的所有回调函数都会在 promise 队列中的回调函数之前执行
总结
注意:要谨慎使用 process.nextTick()。过度使用此方法可能会导致事件循环陷入饥饿状态,从而阻止队列的其余部分运行。官方文档建议使用process.nextTick() 的场景主要有两个:
- Allow users to handle errors, cleanup any then unneeded resources, or perhaps try the request again before the event loop continues.
- At times it's necessary to allow a callback to run after the call stack has unwound but before the event loop continues.
这三个事例主要是说明了两件事:所有用户编写的同步 JavaScript 代码都优先于异步代码执行;nextTick 队列中的所有回调函数都会在 promise 队列中的回调函数之前执行。
原文
Visualizing nextTick and Promise Queues in Node.js Event Loop