一个叫木头,一个叫马尾

深入探讨Node.js中的队列

文章译自: A deep dive into queues in Node.js[1],原作者 Dillion Megida 。

队列(queueing)是Node.js中用于有效处理异步操作的重要技术。

我们将深入了解Node.js中的队列:它们是什么,它们是如何工作的(与Event Loop一起),以及它们的各种类型。

What are queues in Node.js? (Node.js中的队列是什么?)

队列是Node.js中用来适当组织异步操作的数据结构。这些操作以不同的形式存在,包括HTTP请求、文件的读或写操作、流(stream)等等。

Node.js中处理异步操作有时会是一个挑战。

在HTTP请求过程中,根据网络强度的不同,可能会出现不可预知的延迟(或者更糟的是,没有结果)。在尝试用Node.js读取或写入文件时,也可能会出现延迟,这取决于文件的大小。

与定时器(timers)和许多其他操作类似,异步操作完成所需要的时间也是不确定的。

对于这些耗时不同的操作,Node.js需要一种方式来有效地处理它们。

Node.js不能简单地按照 先开始-先处理先完成-先处理 来搞。

这可能不是一个好的选择的原因之一是,一个异步操作可能包含另一个异步操作。

为第一个异步操作留有余地,意味着在考虑队列中的其他异步操作之前,必须先完成其内部的异步操作。(即影响了并行执行异步操作的效率)

有很多情况需要考虑,所以最好的选择是确立一个规则。这个规则会影响Node.js中Event Loop和队列的工作方式。

我们简单地看看Node.js如何处理异步操作。

The call stack, event loop, and callback queues (调用栈、事件循环和回调队列)

调用栈跟踪当前正在执行的函数,以及它从哪里运行。当一个函数即将被执行时,它就会被添加到调用栈中。

这有助于JavaScript在执行函数后回溯其步骤。

回调队列是指当异步操作在后台完成后,用来保存回调函数的队列

它们以 先入先出(FIFO) 的方式工作。我们将在后面的内容中查看不同类型的回调队列。

请注意,Node.js负责每一个异步活动,因为JavaScript因其单线程的特性会阻塞线程。

它还负责在完成后台(异步)操作后向回调队列添加回调函数。JavaScript与回调队列无关。

同时,事件循环不断检查调用栈是否为空,以便从回调队列中拾取一个函数并添加到调用栈中。事件循环只有在所有同步操作执行完毕后才会检查队列。

那么,事件循环按照什么顺序从队列中选择回调函数呢?

首先,我们来看一下回调队列的五大类型。

Types of callback queues (回调队列的类型)

IO queue (IO队列)

IO操作是指涉及外部设备的操作。常见的包括读写文件操作、网络操作等。

这些操作应该是异步的,因为它们是留给Node.js处理的。JavaScript无法访问计算机的内部结构(computer’s internals)。

当要进行这样的操作时,JavaScript会将它们转交给Node.js,后者在后台进行处理。

后台任务结束后,它们(IO操作传入的回调函数)被添加到IO回调队列中,以便让事件循环将其转移到调用栈中执行。

Timer queue (定时器队列)

每一个涉及Node.js定时器功能[2]的操作(比如setTimeout()setInterval())都会被添加到定时器队列中。

请注意,JavaScript本身没有定时器功能[3]

它使用Node.js提供的定时器API(包括setTimeout)来执行与时间相关的操作。为此,定时器操作是异步的。

无论是2秒还是0秒的计时时间,JavaScript都会将与时间相关的操作交给Node.js,然后完成这些操作并添加回调到定时器队列中。

例如:

setTimeout(function ({
  console.log("setTimeout");
}, 0);
console.log("yeah");

运行结果:

yeah
setTimeout

当异步操作被处理时,JavaScript会继续进行其他操作。只有当所有同步操作处理完毕后,事件循环才会进入回调队列

Microtask queue (微任务队列)

这个队列分成两个队列:

关于微任务队列,需要注意的一个重要特征是,事件循环在转向其他队列之前,会反复检查和执行微任务队列中的函数。

例如,当微任务队列完成后,比如说,一个定时器操作执行了一个Promise操作,事件循环会先运行这个Promise操作,然后再转到定时器队列中的其他函数。

因此,微任务队列比其他队列具有更高优先级。

Check queue (also known as immediate queue,检查队列)

该队列中的回调函数会在IO队列中的所有回调函数执行完毕后立即执行。

setImmediate是用来给这个队列添加函数的函数。

例如:

const fs = require("fs");
setImmediate(function ({
  console.log("setImmediate");
});
// assume this operation takes 1ms
fs.readFile("path-to-file"function ({
  console.log("readFile");
});
// assume this operation takes 3ms
do...while...

当这个程序执行时,Node.js会将setImmediate的回调函数添加到检查队列中。由于整个程序还没有完成,事件循环不会检查任何一个队列。

readFile操作是异步的,所以交给Node.js,程序继续执行。

do while操作持续3ms。在这段时间内,readFile操作完成并被推送到IO队列中。完成该操作后,事件循环开始检查队列。

虽然检查队列先被填充,但只有在IO队列空了之后才会考虑。因此,readFile会在setImmediate之前被打印到控制台。

Close queue (关闭队列/结束队列)

该队列存储与关闭事件(close event)操作相关的函数。

例子有:

这些被认为是优先级最低的队列,因为这里的操作发生在较晚的时间。

你通常不会想在处理一个Promise函数之前,在关闭事件中执行一个回调函数。既然服务器已经关闭了,那个Promise还能做什么呢?

Order of the queues (各队列的优先级)

微任务队列的优先级最高,其次是定时器队列、I/O队列、检查队列,最后是关闭队列。


An example of callback queues (回调队列的一个例子)

我们看一个更大的例子来说明队列的类型和顺序(原文中毫秒和秒用混了,这里统一为毫秒):

const fs = require("fs");

// assume this operation takes a 2ms
fs.writeFile("./new-file.json""..."function ({
  console.log("writeFile");
});

// assume this takes 10ms to complete
fs.readFile("./file.json"function (err, data{
  console.log("readFile");
});

// don't assume, this actually takes 1ms
setTimeout(function ({
  console.log("setTimeout");
}, 1);

// assume this operation takes 3ms
while(...) {
    ...
}

setImmediate(function ({
  console.log("setImmediate");
});

// promise that takes 4ms to resolve
let promise = new Promise(function (resolve, reject{
  setTimeout(function ({
    return resolve("promise");
  }, 4);
});
promise.then(function (response{
  console.log(response);
});

console.log("last line");

你能预估出上述代码的打印顺序吗?

答案:

"last line"
"setTimeout"
"writeFile"
"setImmediate"
"promise"
"readFile"

如果需要解释,可在留言中说明或点击 阅读原文

总结

JavaScript是单线程的。每一个异步函数都是由Node.js与计算机的内部功能协作处理的。

Node.js负责在回调队列中添加回调函数(由JavaScript追加到异步操作上的函数)。事件循环决定了每次迭代时下一步要执行的回调函数。

了解Node.js中的队列是如何工作的,可以让你更好地理解它,因为队列是该运行时的核心功能之一。对Node.js最流行的定义是非阻塞(non-blocking),这意味着异步操作会得到妥善处理。

而这正是通过事件循环和回调队列来实现的。

[1]

A deep dive into queues in Node.js: https://blog.logrocket.com/a-deep-dive-into-queues-in-node-js/

[2]

Node.js中定时器: https://nodejs.org/en/docs/guides/timers-in-node/

[3]

JavaScript does not have a timer feature by itself: https://dillionmegida.com/p/browser-apis-and-javascript/#javascript-on-nodejs