理解Node.js中Event Loop(二)

理解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

index.js
console.log("console.log 1");
process.nextTick(() => console.log("this is process.nextTick 1"));
console.log("console.log 2");

在这里,我们有一段最简单的代码来打印三个不同的语句。第二条语句使用 process.nextTick() 方法将回调函数放入 nextTick 队列中。

可视化:

  1. 第一个 console.log() 语句通过被推入执行栈来执行。它在控制台中打印相应的消息,然后从堆栈中弹出。

  2. 接下来, process.nextTick() 在执行栈上执行。这会将回调函数放到 nextTick 队列中并弹出。由于仍然有同步代码要执行,因此回调函数必须等待主线程空闲后执行。

  3. 执行继续进行,最后一个 console.log() 语句被压入堆栈。该消息将打印到控制台,并且该函数将从堆栈中弹出。现在,已经没有需要执行的同步代码,因此控制权交给事件循环。

  4. nextTick队列中的回调函数被压入栈, console.log() 被压入栈并执行,相应的消息被打印到控制台。

由实验1推断

所有用户编写的同步 JavaScript 代码都优先于运行时希望最终执行的异步代码。

让我们继续进行第二个实验。

实验2

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. 当执行栈执行第 1 行时,它将回调函数放入 Promise 队列中。

  2. 当执行栈执行第 2 行时,它将回调函数放入 nextTick 队列中。

  3. 第 2 行之后不再需要执行用户编写的代码。控制交给事件循环,其中 nextTick 队列的优先级高于 Promise 队列(这就是 Node.js 运行时的工作方式)。

  4. 事件循环执行 nextTick 队列回调函数,然后执行 Promise 队列回调函数。

  5. 控制台依次打印“this is Promise.resolve 1”,然后是“this is process.nextTick 1”。

由实验2推断

nextTick 队列中的所有回调(包括递归加入的)都会在 Promise 队列中的回调之前执行。

让我引导您完成上述第二个实验的更详细的版本。

实验3

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 个回调

  1. 众所周知, nextTick 队列具有优先级。首先执行第一个回调,并将相应的消息记录到控制台。

  2. 接下来,执行第二个回调函数,记录第二条日志语句。但是,此回调函数包含对 process.nextTick() 的另一个调用,它将log语句排列到 nextTick 队列的末尾。

  3. 然后,node执行第三个 nextTick 回调,将相应的消息记录到控制台。最初,只有三个回调,但第二个回调向队列中添加了另一个回调。

  4. 事件循环推送内部 nextTick 回调,并执行console.log() 语句。

  5. nextTick 队列为空,控制权转到 Promise 队列。 Promise 队列类似于 nextTick 队列。

  6. 首先,记录“Promise.resolve 1”,然后记录“Promise.resolve 2”。但是,通过调用 process.nextTick() 将函数添加到 nextTick 队列中。尽管如此,控制权仍保留在Promise队列中并继续执行其他回调函数。然后打印 Promise.resolve 3,此时,promise 队列为空。

  7. Node 将再次检查微任务队列中是否有新的回调。由于 nextTick 队列中有一个,因此它会执行该队列,从而生成最后一条log语句。

由实验3推断

nextTick 队列中的所有回调都会在 Promise 队列中的所有回调之前执行。

使用 process.nextTick() 时要小心,过度使用此方法可能会阻止队列的其他部分运行。大量 nextTick() 调用,会阻止 I/O 队列执行自己的回调。


结论

实验表明,同步 JavaScript 代码都优先于运行时最终执行的异步代码,且 nextTick 队列中的所有回调都会在 Promise 队列中的所有回调之前执行。


此系列文章:

理解Node.js中Event Loop(一)

理解Node.js中Event Loop(二)

理解Node.js中Event Loop(三)

理解Node.js中Event Loop(四)

理解Node.js中Event Loop(五)

理解Node.js中Event Loop(六)

理解Node.js中Event Loop(七)