深入浅析Node事件循环中的微任务队列
在 之前的文章 中,我们了解到事件循环是 Node.js 的一个关键部分,用于协调同步和异步代码的执行。
它由六个不同的队列组成。一个 nextTick 队列和一个 Promise 队列(被称为微任务队列)、一个计时器队列、一个 I/O 队列、一个检查队列,最后是关闭队列。【相关教程推荐:nodejs视频教程、编程教学】
在每次循环中,回调函数会在适当时出队(dequeued)并在调用栈上执行。从本文开始,我们会进行一些实验以确保我们对事件循环的理解是正确的。
我们的第一组实验,将重点关注 nextTick 队列 和 Promise 队列。但在深入实验之前,需要先了解如何将回调函数排到队列中。
注意:所有实验都是使用 CommonJS 模块格式进行的。
入队回调函数
要将回调函数加入到 nextTick 队列中,我们可以使用内置的 process.nextTick()
方法。语法很简单:process.nextTick(callbackFn)
。当该方法在调用栈上执行时,回调函数将被加入到 nextTick 队列中。
要将回调函数加入到 Promise 队列中,我们需要使用 Promise.resolve().then(callbackFn)
。当 Promise 解决时(resolve),传递给 then()
的回调函数将被入队到 Promise 队列中。
现在我们已经了解了如何向两个队列添加回调函数,让我们开始第一个实验吧。
实验一
代码
// 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()
被推入调用栈并且执行,在控制台记录相应的消息。
console.log 1 console.log 2 this is process.nextTick 1登录后复制
推论
所有用户编写的同步 JavaScript 代码优先于异步代码执行。
让我们继续进行第二个实验。
实验二
代码
// 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”。
this is process.nextTick 1 this is Promise.resolve 1登录后复制
推论
nextTick 队列中的所有回调函数优先于 Promise 队列中的回调函数执行。
让我带你走一遍上述第二个实验的更详细版本。
福利实验
代码
// 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()
语句,每个都带有一个回调函数。
可视化
为了加快对此可视化的解释,我将省略调用栈讲解。当调用栈执行所有 6 个语句时,nextTick 队列中有 3 个回调函数,Promise 队列中也有 3 个回调函数。没有其他要执行的内容后,控制权进入事件循环。
我们知道,nextTick 队列中具有高优先级。首先执行第 1 个回调函数,并将相应的消息记录到控制台中。
接下来,执行第 2 个回调函数,记录了第 2 条日志语句。但是,这个回调函数包含另一个对 process.nextTick()
的调用,该方法内的回调函数(译注:即() => console.log("this is the inner next tick inside next tick")
)入队到nextTick 队列末尾。
然后 Node.js 执行第 3 个回调函数并将相应消息记录到控制台上。最初只有 3 个回调,但是因为第 2 次时向nextTick 队列又添加了一个,所以变成 4 个了。
事件循环将第 4 个回调函数推入调用栈,执行 console.log()
语句。
接着处理完 nextTick 队列之后,控制流程进入到 Promise 队列。Promise 队列与 nextTick 队列处理方式类似。
首先记录“Promise.resolve 1”,然后是“Promise.resolve 2”,这时因为调用 process.nextTick() 的原因,一个函数(译注:即 () => console.log("this is the inner next tick inside Promise then block")
) 被推入 nextTick 队列了。尽管如此,控制流程仍停留在 Promise 队列,因此还会继续执行队列中的其他函数。然后我们得到“Promise.resolve 3”,此时 Promise 队列为空。
Node.js 将再次检查微任务队列中是否有新的回调。由于 nextTick 队列中有一个回调,因此会执行这个回调,导致我们的最后一条日志输出。
this is process.nextTick 1 this is process.nextTick 2 this is process.nextTick 3 this is the inner next tick inside next tick this is Promise.resolve 1 this is Promise.resolve 2 this is Promise.resolve 3 this is the inner next tick inside Promise then block登录后复制
这可能是一个稍微高级的实验,但推论仍然相同。
推论
nextTick 队列中的所有回调函数优先于 Promise 队列中的回调函数执行。
使用 process.nextTick()
时要小心。过度使用此方法可能会导致事件循环饥饿,从而阻止队列中的其余部分运行。当存在大量的 nextTick() 调用时,I/O 队列是无法执行自己的回调函数的。官方文档建议使用 process.nextTick()
的两个主要场景:处理错误或在调用栈为空事件循环继续之前执行回调用。所以在使用 process.nextTick()
时,要明智一些。
总结
实验表明,用户编写的所有同步 JavaScript 代码优先于异步代码执行,并且 nextTick 队列中的所有回调函数优先于 Promise 队列中的回调函数执行。
原文链接:Visualizing nextTick and Promise Queues in Node.js Event Loop,2023年3月30日,by Vishwas Gopinath
更多node相关知识,请访问:nodejs 教程!
【本文来源:香港服务器租用 http://www.558idc.com/st.html欢迎留下您的宝贵建议】