Node.js 事件循环机制

Node.js Event-Loop Mechanism

我正在学习Node.js中的Event-Loop机制,我正在做一些练习,但有一些困惑,如下所述。

const fs = require("fs");

setTimeout(() => console.log("Timer 1"), 0);
setImmediate(() => console.log("Immediate 1"));

fs.readFile("test-file-with-1-million-lines.txt", () => {
  console.log("I/O");

  setTimeout(() => console.log("Timer 2"), 0);
  setTimeout(() => console.log("Timer 3"), 3000);
  setImmediate(() => console.log("Immediate 2"));
});

console.log("Hello");

我希望看到以下输出:

你好
计时器 1
立即数 1
I/O
计时器 2
立即数 2
计时器 3

但我得到以下输出:

你好
计时器 1
立即数 1
I/O
立即数 2
计时器 2
计时器 3

请你解释一下这些行是如何一步步执行的。

此输出的原因是 javascript 的异步性质。

  • 您将前 2 个输出设置为某种超时,执行时间为 0,这使它们仍然等待滴答。
  • 接下来您要读取文件,这需要一段时间才能完成,因此会延迟回调中函数的执行
  • 回调中的第一个 console.log 会在回调执行后立即触发,回调中的其余部分遵循代码的第一部分
  • 最后,底部的 console.log 首先执行,因为它没有延迟,不需要等到下一个订单号。

作为一些额外的帮助,请观看此视频。

https://youtu.be/cCOL7MC4Pl0

主持人就事件循环发表了精彩的演讲。我认为这是一个很好的资源。

虽然这是专门针对浏览器的,但在 Node 中共享许多方面。

首先,我要提一下,如果您真的希望异步操作 A 以与异步操作 B 相关的特定顺序进行处理,您可能应该编写代码以保证不依赖于异步操作 B 的细节究竟是什么首先到达 运行。但是,也就是说,我 运行 遇到这样的问题,即一种类型的异步操作可以“霸占”事件循环并使其他类型的事件挨饿,了解内部真正发生的事情可能很有用 if/when发生了。

归根结底,您的问题实际上是关于为什么 Immediate2Timer2 之前从 I/O 回调中安排记录,但不是从顶级代码调用时?因此不一致。

这与事件循环在其循环中的位置有关,通过在 setTimeout()setImmediate() 被调用时(当它们被调度时)它正在做的各种检查。这里稍微解释一下:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout.

如果你看一下这个事件循环的简化图(来自上面的文章):

您可以看到事件循环有许多不同的部分。 setTimeout() 由图表顶部的“计时器”块提供服务。 setImmediate() 在图表底部附近的“检查”块中提供。文件 I/O 在中间的“轮询”块中提供。

因此,如果您在文件 I/O 回调中同时安排 setImmediate(fn1)setTimeout(fn2, 0)(Intermediate2 和 Timer2 就是这种情况),那么事件循环处理这两个调度的时候恰好处于poll阶段。因此,事件循环的下一阶段是“检查”阶段,setImmediate(fn1) 得到处理。然后,在“检查”阶段和“关闭回调”阶段之后,它会循环回到“计时器”阶段,您会得到 setTimeout(fn2,0).

另一方面,如果您从事件循环的不同阶段从 运行 的代码中调用相同的两个 setImmediate()setTimeout(),则计时器可能在 setImmediate() 之前首先处理 - 这将取决于代码在事件循环周期中执行的确切位置。

事件循环的这种结构就是为什么有人将 setImmediate() 描述为“它 运行 就在 I/O 之后”,因为它位于循环中,在“轮询”阶段。如果您正在 I/O 回调中处理某些文件 I/O 并且希望在堆栈展开后立即 运行 某些内容,您可以使用 setImmediate() 来完成那。它总是 运行 在当前 I/O 回调完成之后,但在计时器之前。

注意:此简化描述中遗漏了具有特殊待遇的承诺。 Promise 被认为是微任务,它们有自己的队列。他们更频繁地到达 运行。从节点 v11 开始,它们在事件循环的每个阶段都达到 运行。因此,如果您有三个准备好 运行 的挂起计时器,并且您进入事件循环的计时器阶段并为第一个挂起计时器调用回调,并且在该计时器回调中,您解决了一个承诺,然后作为一旦那个定时器回调 returns 回到系统,它就会服务于那个已解决的承诺。因此,微任务(例如 promises 和 process.nextTick())在事件循环中的每个操作之间得到服务(如果等待 运行),不仅在事件循环的阶段之间,甚至在事件循环中的未决事件之间同相。您可以在此处阅读有关这些细节和节点 v11 更改的更多信息:New Changes to the Timers and Microtasks in Node v11.0.0 and above.

我相信这样做是为了提高与 promise 相关的代码的性能,因为 promises 已成为异步操作的 nodejs 体系结构的核心部分,并且在该领域也有一些与标准相关的工作来实现这一点在不同的 JS 环境中保持一致。

这是另一个涵盖其中部分内容的参考资料:

Nodejs Event Loop - interaction with top-level code