一文带你了解Node事件循环中的计时器队列

编辑: admin 分类: .net 发布时间: 2023-06-08 来源:互联网

image.png

在 上一篇文章 中,我们探讨了微任务队列及它在各队列中的优先级顺序。在本文,我们将讨论计时器队列,这是 Node.js中用于处理异步代码的另一个队列。

在深入研究计时器队列之前,让我们快速回顾一下微任务队列。为了将回调函数排入微任务队列,我们使用诸如 process.nextTick()Promise.resolve() 等函数。当涉及到执行 Node.js 中的异步代码时,微任务队列具有最高优先级。【相关教程推荐:nodejs视频教程、编程教学】

入队回调函数

现在我们转到计时器队列。要将回调函数排入计时器队列,我们可以使用 setTimeoutsetInterval等函数。为了方便说明,本文将使用 setTimeout

为了理解计时器队列的执行顺序,我们会进行一系列实验,在微任务队列和计时器队列中入队任务。

实验三

代码

// index.js
setTimeout(() => console.log("this is setTimeout 1"), 0);
setTimeout(() => console.log("this is setTimeout 2"), 0);
setTimeout(() => console.log("this is setTimeout 3"), 0);

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"));
登录后复制

译注:大家不用紧张,这段代码就是在上篇“福利实验”的基础上,在开头加了 3 个 setTimeout 语句。

该代码包含三个 process.nextTick() 的调用,三个 Promise.resolve() 的调用和三个 setTimeout 的调用。每个回调函数记录适当的消息。所有三个 setTimeout 调用都有 0ms 的延迟,这表示在执行每个 setTimeout 语句时,回调函数都立即入队到计时器队列等待。第二次 process.nextTick() 和第二次 Promise.resolve() 都有一个额外的 process.nextTick() 语句,并且每个都带有一个回调函数。

可视化

assets_YJIGb4i01jvw0SRdL5Bt_c4034ba006d840128b729005183abdf4_compressed.gif

当调用栈执行所有语句后,在 nextTick 队列中有 3 个回调,在 Promise 队列中有 3 个回调,在计时器队列中也有 3 个回调。没有代码要执行,控制权进入事件循环。

nextTick 队列具有最高优先级,其次是 Promise 队列,然后是计时器队列。从 nextTick 队列中获取第1 个回调并执行它,将一条消息记录到控制台。接着获取第 2 个回调并执行它,这也会记录一条消息。第 2 个回调包含 process.nextTick() 的调用,该方法将新的回调添加到了 nextTick 队列中。继续执行,并获取和执行第 3 个回调以及记录一条消息。最后,我们将新添加到 nextTick 队列的回调函数取出并在调用栈中执行,从而在控制台上输出了第四条日志信息。

当 nextTick 队列为空时,事件循环转向 Promise 队列。从队列中获取第 1 个回调,在控制台打印一条信息,第二个回调效果类似,并且还向 nextTick 队列添加了一个回调。 Promise 中的第 3 个回调被执行,接着日志消息被输出。此时 Promise 队列已空,事件循环检查 nextTick 队列是否存在新的回调,找到之后同样将消息记录到控制台。

现在,两个微任务队列都空了,事件循环转向计时器队列。我们有三个回调,每个回调依次从计时器队列中取出并在调用栈上执行,将分别打印 "setTimeout 1"、"setTimeout 2" 和 "setTimeout 3"。

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
this is setTimeout 1
this is setTimeout 2
this is setTimeout 3
登录后复制

推论

微任务队列中的回调函数会在定时器队列中的回调函数之前执行。

到目前为止,优先顺序是 nextTick 队列,其次是 Promise 队列,然后是定时器队列。现在让我们继续进行下一个实验。

实验四

// index.js
setTimeout(() => console.log("this is setTimeout 1"), 0);
setTimeout(() => {
  console.log("this is setTimeout 2");
  process.nextTick(() =>
    console.log("this is inner nextTick inside setTimeout")
  );
}, 0);
setTimeout(() => console.log("this is setTimeout 3"), 0);

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"));
登录后复制

第四个实验的代码大部分与第三个相同,只有一个例外。传递给第二个 setTimeout 函数的回调函数现在包含 process.nextTick() 的调用。

可视化

assets_YJIGb4i01jvw0SRdL5Bt_c4034ba006d840128b729005183abdf4_compressed (1).gif

让我们应用从之前的实验中学到的知识,快进到回调在微任务队列中已经被执行的点。假设我们有三个回调在计时器队列中排队等待。第一个回调出队并在调用堆栈上执行,“setTimeout 1”消息打印到控制台。事件循环继续运行第二个回调,“setTimeout 2”消息打印到控制台。同时,也会有一个回调函数入队了 nextTick 队列。

在执行计时器队列中的每个回调后,事件循环会返回检查微任务队列。检查 nextTick 队列确定需要执行的回调函数。这时第二个 setTimeout 推入的回调函数出队并在调用栈上执行,结果“inner nextTick”消息打印到控制台。

现在微任务队列为空了,控制权返回到计时器队列,最后一个回调被执行,控制台上显示消息“setTimeout 3”。

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
this is setTimeout 1
this is setTimeout 2
this is inner nextTick inside setTimeout
this is setTimeout 3
登录后复制

推论

微任务队列中的回调函数会在定时器队列中的回调函数执行之间被执行。

实验五

代码

// index.js
setTimeout(() => console.log("this is setTimeout 1"), 1000);
setTimeout(() => console.log("this is setTimeout 2"), 500);
setTimeout(() => console.log("this is setTimeout 3"), 0);
登录后复制

该代码包含三个 setTimeout 语句,包含三个不同的、入队时机不一样的回调函数。第一个 setTimeout 延迟 1000 毫秒,第二个延迟 500 毫秒,第三个延迟 0 毫秒。当执行这些回调函数时,它们只是简单地将一条消息记录到控制台中。

可视化

由于代码片段的执行非常简单,因此我们将跳过可视化实验。当多个 setTimeout 调用被发出时,事件循环首先排队最短延迟的一个并在其他之前执行。结果,我们观察到“setTimeout 3”先执行,然后是“setTimeout 2”,最后是“setTimeout 1”。

this is setTimeout 3
this is setTimeout 2
this is setTimeout 1
登录后复制

推论

计时器队列回调按照先进先出(FIFO)的顺序执行。

总结

实验表明,微任务队列中的回调比定时器队列中的回调具有更高优先级,并且微任务队列中的回调在定时器队列中的回调之间执行。定时器队列遵循先进先出(FIFO)顺序。

原文链接:Visualizing The Timer Queue in Node.js Event Loop,2023年4月4日,by Vishwas Gopinath

更多node相关知识,请访问:nodejs 教程!

更多阅读