理解Node.js中Event Loop(二)
本文翻译至:Visualizing nextTick and Promise Queues in Node.js Event Loop
欢迎阅读可视化 Node.js 事件循环系列的第二篇文章。在第一篇文章中,我们了解到事件循环是 Node.js 的重要组成部分,有助于协调同步和异步代码的执行。
它由六个不同的队列组成。一个 nextTick 队列和一个 Promise 队列(在本系列文章中称为微任务队列)、一个Timer(计时器)队列、一个 I/O 队列、一个Check队列,最后是一个Close队列。
在每个循环中,回调函数会在适当的时候弹出队列并在执行栈上执行。从本文开始,让我们进行一些实验,以确保我们对事件循环的可视化是正确的。
对于我们的第一组实验,我们将重点关注 nextTick 队列和 promise 队列。但在我们深入实验之前,我们首先了解如何在每个队列中对回调函数进行排队。
目录
回调函数的执行顺序
要将回调函数放入 nextTick 队列中,我们使用内置的 process.nextTick()
方法。语法很简单: process.nextTick(callbackFn)
。当该方法在执行栈上执行时,回调函数将被放入 nextTick 队列中。
要将回调函数放入 Promise 队列中,我们将使用 Promise.resolve().then(callbackFn)
。当 Promise 解析时,传入 then()
表达式的函数将放入 Promise 队列中。
现在我们了解了如何向两个队列添加回调函数,让我们开始第一个实验。
实验1
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()
被压入栈并执行,相应的消息被打印到控制台。
由实验1推断
所有用户编写的同步 JavaScript 代码都优先于运行时希望最终执行的异步代码。
让我们继续进行第二个实验。
实验2
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 Promise.resolve 1”,然后是“this is process.nextTick 1”。
由实验2推断
nextTick 队列中的所有回调(包括递归加入的)都会在 Promise 队列中的回调之前执行。
让我引导您完成上述第二个实验的更详细的版本。
实验3
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()
语句,每个语句都有一个回调函数。
可视化:
为了加快对该可视化的解释,我将省略执行栈执行同步代码过程。当执行栈执行全部 6 个语句时, nextTick 队列中有 3 个回调,promise 队列中有 3 个回调。
众所周知, nextTick 队列具有优先级。首先执行第一个回调,并将相应的消息记录到控制台。
接下来,执行第二个回调函数,记录第二条日志语句。但是,此回调函数包含对
process.nextTick()
的另一个调用,它将log语句排列到 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 队列中有一个,因此它会执行该队列,从而生成最后一条log语句。
由实验3推断
nextTick 队列中的所有回调都会在 Promise 队列中的所有回调之前执行。
使用 process.nextTick()
时要小心,过度使用此方法可能会阻止队列的其他部分运行。大量 nextTick()
调用,会阻止 I/O 队列执行自己的回调。
结论
实验表明,同步 JavaScript 代码都优先于运行时最终执行的异步代码,且 nextTick 队列中的所有回调都会在 Promise 队列中的所有回调之前执行。
此系列文章: