欢迎来到我们系列文章的第三篇,关于可视化
Node.js
事件循环。在上一篇文章中,我们探讨了微任务队列及其在执行异步代码时的优先级顺序。在本文中,我们将讨论定时器队列,这是 Node.js
中用于处理异步代码的另一个队列。
在我们深入研究定时器队列之前,让我们快速回顾一下微任务队列。要将回调函数排入微任务队列,我们使用诸如 process.nextTick()
和 Promise.resolve()
这样的函数。微任务队列在执行 Node.js 中的异步代码时具有最高的优先级。
回调函数队列
现在让我们转向定时器队列。要将回调函数排入定时器队列,我们可以使用 setTimeout
和 setInterval
等函数。在本博客文章中,我们将使用 setTimeout
。
为了理解定时器队列中的执行顺序,让我们进行一系列实验。我们将在微任务队列和定时器队列中排队任务。
实验 3
// index.js
setTimeout(() => console.log("这是 setTimeout 1"), 0);
setTimeout(() => console.log("这是 setTimeout 2"), 0);
setTimeout(() => console.log("这是 setTimeout 3"), 0);
process.nextTick(() => console.log("这是 process.nextTick 1"));
process.nextTick(() => {
console.log("这是 process.nextTick 2");
process.nextTick(() =>
console.log("这是内部的下一个 tick,位于下一个 tick 内部")
);
});
process.nextTick(() => console.log("这是 process.nextTick 3"));
Promise.resolve().then(() => console.log("这是 Promise.resolve 1"));
Promise.resolve().then(() => {
console.log("这是 Promise.resolve 2");
process.nextTick(() =>
console.log("这是内部的下一个 tick,位于 Promise then 块内部")
);
});
Promise.resolve().then(() => console.log("这是 Promise.resolve 3"));
这段代码包含三次对process.nextTick()
的调用、三次对 Promise.resolve()
的调用和三次对setTimeout
的调用。每个回调函数记录了相应的消息。所有三个 setTimeout
调用的延迟为 0 毫秒,这意味着每个 setTimeout
语句在调用堆栈上执行时,回调函数都会立即排队。第二个 process.nextTick()
和第二个 Promise.resolve()
都有额外的 process.nextTick()
语句,每个都有一个回调函数。
当调用堆栈执行所有语句时,我们得到了三个回调函数在
nextTick
队列中,三个在Promise
队列中,以及三个在定时器队列中。没有进一步的代码需要执行,控制进入事件循环。
nextTick
队列具有最高的优先级,其次是 Promise
队列,然后是定时器队列。从 nextTick
队列中出列并执行第一个回调函数,记录一条消息到控制台。然后,第二个回调函数被出列并执行,也记录了一条消息。第二个回调函数包含对process.nextTick()
的调用,它向nextTick
队列添加了一个新的回调函数。执行继续,第三个回调函数被出列并执行,同样记录了一条消息。最后,新添加的回调函数被出列并在调用堆栈上执行,导致控制台中的第四条日志消息。
nextTick
队列为空后,事件循环转移到Promise
队列。第一个回调函数在调用堆栈上出列并执行,将一条消息打印到控制台。第二个回调具有类似的效果,并且还向 nextTick
队列添加了一个回调函数。在 Promise
中的第三个回调函数被执行,导致下一条日志消息。此时,Promise
队列为空,事件循环检查 nextTick
队列是否有新的回调函数。找到一个,它也被执行并记录一条消息到控制台。
现在,两个微任务队列都为空了,事件循环转移到定时器队列。我们有三个回调函数,它们逐个在调用堆栈上出列并执行。这将打印出 "setTimeout 1"
、"setTimeout 2"
和 "setTimeout 3"
。
微任务队列中的回调函数在定时器队列中的回调函数之前执行。
好的,到目前为止,优先级顺序是 nextTick
队列,然后是 Promise
队列,最后是定时器队列。现在让我们继续进行下一个实验。
实验 4
// index.js
setTimeout(() => console.log("这是 setTimeout 1"), 0);
setTimeout(() => {
console.log("这是 setTimeout 2");
process.nextTick(() =>
console.log("这是 setTimeout 中的内部 nextTick")
);
}, 0);
setTimeout(() => console.log("这是 setTimeout 3"), 0);
process.nextTick(() => console.log("这是 process.nextTick 1"));
process.nextTick(() => {
console.log("这是 process.nextTick 2");
process.nextTick(() =>
console.log("这是下一个 next tick 内部的内部 next tick")
);
});
process.nextTick(() => console.log("这是 process.nextTick 3"));
Promise.resolve().then(() => console.log("这是 Promise.resolve 1"));
Promise.resolve().then(() => {
console.log("这是 Promise.resolve 2");
process.nextTick(() =>
console.log("这是 Promise then 块中的内部 next tick")
);
});
Promise.resolve().then(() => console.log("这是 Promise.resolve 3"));
第四个实验的代码与第三个实验的代码大部分相同,只有一个例外。现在,传递给第二个setTimeout
函数的回调函数包括对 process.nextTick()
的调用。
让我们应用从上一个实验中学到的知识,并快进到微任务队列中的回调函数已经执行的时刻。假设我们在定时器队列中排队了三个回调函数。第一个回调函数被出列并在调用堆栈上执行,导致控制台打印出一条
"setTimeout 1"
消息。事件循环继续并执行第二个回调函数,导致控制台打印出"setTimeout 2"
消息。然而,这也在 nextTick
队列中排队了一个回调函数。
在执行定时器队列中的每个回调函数后,事件循环会返回并检查微任务队列。它检查 nextTick
队列并识别需要执行的回调函数。这个回调函数被出
列并在调用堆栈上执行,导致控制台打印出 "内部 nextTick"
消息。
现在微任务队列为空了,控制权回到定时器队列,最后一个回调被执行,导致控制台上打印出"setTimeout 3"
消息。
微任务队列中的回调函数在执行定时器队列中的回调函数之间执行。
实验 5
// index.js
setTimeout(() => console.log("这是 setTimeout 1"), 1000);
setTimeout(() => console.log("这是 setTimeout 2"), 500);
setTimeout(() => console.log("这是 setTimeout 3"), 0);
这段代码包含了三个 setTimeout
语句,排队了三个不同的回调函数。第一个 setTimeout
延迟了 1000 毫秒,第二个延迟了500
毫秒,第三个延迟了0
毫秒。回调函数在执行时只是简单地将一条消息记录到控制台中。
代码片段的执行非常简单。当进行多个 setTimeout
调用时,事件循环首先排队最短延迟的那个,并在其他调用之前执行它。因此,我们首先观察到 "setTimeout 3"
被执行,然后是"setTimeout 2"
,最后是 "setTimeout 1"
。
定时器队列中的回调函数按照先进先出(FIFO)的顺序执行。
结论
实验表明,微任务队列中的回调函数优先级高于定时器队列中的回调函数,并且微任务队列中的回调函数在执行定时器队列中的回调函数之间执行。定时器队列遵循先进先出(FIFO
)的顺序。