// 每日前端夜话 第365篇
// 正文共:3000 字
// 预计阅读时间:10 分钟

队列是 Node.js 中用于有效处理异步操做的一项重要技术。在本文中,咱们将深刻研究 Node.js 中的队列:它们是什么,它们如何工做(经过事件循环)以及它们的类型。javascript
Node.js 中的队列是什么?
队列是 Node.js 中用于组织异步操做的数据结构。这些操做以不一样的形式存在,包括HTTP请求、读取或写入文件操做、流等。html
在 Node.js 中处理异步操做很是具备挑战性。前端
HTTP 请求期间可能会出现不可预测的延迟(或者更糟糕的可能性是没有结果),具体取决于网络质量。尝试用 Node.js 读写文件时也有可能会产生延迟,具体取决于文件的大小。java
相似于计时器和其余的许多操做,异步操做完成的时间也有多是不肯定的。node
在这些不一样的延迟状况之下,Node.js 须要可以有效地处理全部这些操做。web
Node.js 没法处理基于 first-start-first-handle (先开始先处理)或 first-finish-first-handle (先结束先处理)的操做。面试
之因此不能这样作的一个缘由是,在一个异步操做中可能还会包含另外一个异步操做。json
为第一个异步过程留出空间意味着必须先要完成内部异步过程,而后才能考虑队列中的其余异步操做。api
有许多状况须要考虑,所以最好的选择是制定规则。这个规则影响了事件循环和队列在 Node.js 中的工做方式。promise
让咱们简要地看一下 Node.js 是怎样处理异步操做的。
调用栈,事件循环和回调队列
调用栈被用于跟踪当前正在执行的函数以及从何处开始运行。当一个函数将要执行时,它会被添加到调用堆栈中。这有助于 JavaScript 在执行函数后从新跟踪其处理步骤。
回调队列是在后台操做完成时把回调函数保存为异步操做的队列。它们以先进先出(FIFO)的方式工做。咱们将会在本文后面介绍不一样类型的回调队列。
请注意,Node.js 负责全部异步活动,由于 JavaScript 能够利用其单线程性质来阻止产生新的线程。
在完成后台操做后,它还负责向回调队列添加函数。JavaScript 自己与回调队列无关。同时事件循环会连续检查调用栈是否为空,以即可以从回调队列中提取一个函数并添加到调用栈中。事件循环仅在执行全部同步操做以后才检查队列。
那么,事件循环是按照什么样的顺序从队列中选择回调函数的呢?
首先,让咱们看一下回调队列的五种主要类型。
回调队列的类型
IO 队列(IO queue)
IO操做是指涉及外部设备(如计算机的硬盘、网卡等)的操做。常见的操做包括读写文件操做、网络操做等。这些操做应该是异步的,由于它们留给 Node.js 处理。
JavaScript 没法访问计算机的内部设备。当执行此类操做时,JavaScript 会将其传输到 Node.js 以在后台处理。
完成后,它们将会被转移到 IO 回调队列中,来进行事件循环,以转移到调用栈中执行。
计时器队列(Timer queue)
每一个涉及 Node.js 计时器功能[1]的操做(如 setTimeout()
和 setInterval()
)都是要被添加到计时器队列的。
请注意,JavaScript 语言自己没有计时器功能[2]。它使用 Node.js 提供的计时器 API(包括 setTimeout
)执行与时间相关的操做。因此计时器操做是异步的。不管是 2 秒仍是 0 秒,JavaScript 都会把与时间相关的操做移交给 Node.js,而后将其完成并添加到计时器队列中。
例如:
setTimeout(function() {
console.log('setTimeout');
}, 0)
console.log('yeah')
# 返回
yeah
setTimeout
在处理异步操做时,JavaScript 会继续执行其余操做。只有在全部同步操做都已被处理完毕后,事件循环才会进入回调队列。
微任务队列(Microtask queue)
该队列分为两个队列:
-
第一个队列包含因 process.nextTick
函数而延迟的函数。
事件循环执行的每一个迭代称为一个 tick(时间刻度)。
process.nextTick
是一个函数,它在下一个 tick (即事件循环的下一个迭代)执行一个函数。微任务队列须要存储此类函数,以即可以在下一个 tick 执行它们。
这意味着事件循环必须继续检查微任务队列中的此类函数,而后再进入其余队列。
-
第二个队列包含因 promises
而延迟的函数。
如你所见,在 IO 和计时器队列中,全部与异步操做有关的内容都被移交给了异步函数。
可是 promise 不一样。在 promise 中,初始变量存储在 JavaScript 内存中(你可能已经注意到了<Pending>
)。
异步操做完成后,Node.js 会将函数(附加到 Promise)放在微任务队列中。同时它用获得的结果来更新 JavaScript 内存中的变量,以使该函数不与 <Pending>
一块儿运行。
如下代码说明了 promise 是如何工做的:
let prom = new Promise(function (resolve, reject) {
// 延迟执行
setTimeout(function () {
return resolve("hello");
}, 2000);
});
console.log(prom);
// Promise { <pending> }
prom.then(function (response) {
console.log(response);
});
// 在 2000ms 以后,输出
// hello
关于微任务队列,须要注意一个重要功能,事件循环在进入其余队列以前要反复检查并执行微任务队列中的函数。例如,当微任务队列完成时,或者说计时器操做执行了 Promise 操做,事件循环将会在继续进入计时器队列中的其余函数以前参与该 Promise 操做。
所以,微任务队列比其余队列具备最高的优先级。
检查队列(Check queue)
检查队列也称为即时队列(immediate queue)。IO 队列中的全部回调函数均已执行完毕后,当即执行此队列中的回调函数。setImmediate 用于向该队列添加函数。
例如:
const fs = require('fs');
setImmediate(function() {
console.log('setImmediate');
})
// 假设此操做须要 1ms
fs.readFile('path-to-file', function() {
console.log('readFile')
})
// 假设此操做须要 3ms
do...while...
执行该程序时,Node.js 把 setImmediate
回调函数添加到检查队列。因为整个程序还没有准备完毕,所以事件循环不会检查任何队列。
由于 readFile
操做是异步的,因此会移交给 Node.js,以后程序将会继续执行。
do while
操做持续 3ms。在这段时间内,readFile
操做完成并被推送到 IO 队列。完成此操做后,事件循环将会开始检查队列。
尽管首先填充了检查队列,但只有在 IO 队列为空以后才考虑使用它。因此在 setImmediate
以前,将 readFile
输出到控制台。
关闭队列(Close queue)
此队列存储与关闭事件操做关联的函数。
包括如下内容:
-
流关闭事件 [3],在关闭流时发出。它表示再也不发出任何事件。 -
http关闭事件 [4],在服务器关闭时发出。
这些队列被认为是优先级最低的,由于此处的操做会在之后发生。
你肯sing不但愿在处理 promise 函数以前在 close 事件中执行回调函数。当服务器已经关闭时,promise 函数会作些什么呢?
队列顺序
微任务队列具备最高优先级,其次是计时器队列,I/O队列,检查队列,最后是关闭队列。
回调队列的例子
让咱们经过一个更复杂的例子来讲明队列的类型和顺序:
const fs = require("fs");
// 假设此操做须要 2ms
fs.writeFile('./new-file.json', '...', function() {
console.log('writeFile')
})
// 假设这须要 10ms 才能完成
fs.readFile("./file.json", function(err, data) {
console.log("readFile");
});
// 不须要假设,这实际上须要 1ms
setTimeout(function() {
console.log("setTimeout");
}, 1000);
// 假设此操做须要 3ms
while(...) {
...
}
setImmediate(function() {
console.log("setImmediate");
});
// 解决 promise 须要 4 ms
let promise = new Promise(function (resolve, reject) {
setTimeout(function () {
return resolve("promise");
}, 4000);
});
promise.then(function(response) {
console.log(response)
})
console.log("last line");
程序流程以下:
-
在 0 毫秒时,程序开始。
-
在 Node.js 将回调函数添加到 IO 队列以前,
fs.writeFile
在后台花费 2 毫秒。
fs.readFile
takes 10ms at the background before Node.js adds the callback function to the IO queue.
-
在 Node.js 将回调函数添加到 IO 队列以前,
fs.readFile
在后台花费 10 毫秒。 -
在 Node.js 将回调函数添加到计时器队列以前,
setTimeout
在后台花费 1ms。 -
如今,while 操做(同步)须要 3ms。在此期间,线程被阻止(请记住 JavaScript 是单线程的)。
-
一样在这段时间内,
setTimeout
和fs.writeFile
操做完成,并将它们的回调函数分别添加到计时器和 IO 队列中。
如今的队列是:
// queues
Timer = [
function () {
console.log("setTimeout");
},
];
IO = [
function () {
console.log("writeFile");
},
];
setImmediate
将回调函数添加到 Check 队列中:
js
// 队列
Timer...
IO...
Check = [
function() {console.log("setImmediate")}
]
在将 promise 操做添加到微任务队列以前,须要花费 4ms 的时间在后台进行解析。
最后一行是同步的,所以将会当即执行:
# 返回
"last line"
由于全部同步活动都已完成,因此事件循环开始检查队列。因为微任务队列为空,所以它从计时器队列开始:
// 队列
Timer = [] // 如今是空的
IO...
Check...
# 返回
"last line"
"setTimeout"
当事件循环继续执行队列中的回调函数时,promise
操做完成并被添加到微任务队列中:
// 队列
Timer = [];
Microtask = [
function (response) {
console.log(response);
},
];
IO = []; // 当前是空的
Check = []; // 当前是在 IO 的后面,为空
# results
"last line"
"setTimeout"
"writeFile"
"setImmediate"
几秒钟后,readFile
操做完成,并添加到 IO 队列中:
// 队列
Timer = [];
Microtask = []; // 当前是空的
IO = [
function () {
console.log("readFile");
},
];
Check = [];
# results
"last line"
"setTimeout"
"writeFile"
"setImmediate"
"promise"
最后,执行全部回调函数:
// 队列
Timer = []
Microtask = []
IO = [] // 如今又是空的
Check = [];
# results
"last line"
"setTimeout"
"writeFile"
"setImmediate"
"promise"
"readFile"
这里要注意的三点:
-
异步操做取决于添加到队列以前的延迟时间。并不取决于它们在程序中的存放顺序。 -
事件循环在每次迭代之继续检查其余任务以前,会连续检查微任务队列。 -
即便在后台有另外一个 IO 操做( readFile
),事件循环也会执行检查队列中的函数。这样作的缘由是此时 IO 队列为空。请记住,在执行 IO 队列中的全部的函数以后,将会当即运行检查队列回调。
总结
JavaScript 是单线程的。每一个异步函数都由依赖操做系统内部函数工做的 Node.js 去处理。
Node.js 负责将回调函数(经过 JavaScript 附加到异步操做)添加到回调队列中。事件循环会肯定将要在每次迭代中接下来要执行的回调函数。
了解队列如何在 Node.js 中工做,使你对其有了更好的了解,由于队列是环境的核心功能之一。Node.js 最受欢迎的定义是 non-blocking
(非阻塞),这意味着异步操做能够被正确的处理。都是由于有了事件循环和回调队列才能使此功能生效。
做者:Dillion Megida
翻译:疯狂的技术宅
原文:https://blog.logrocket.com/a-deep-dive-into-queues-in-node-js/
Reference
Node.js 计时器功能: https://nodejs.org/en/docs/guides/timers-in-node/
[2]JavaScript 语言自己没有计时器功能: https://dillionmegida.com/p/browser-apis-and-javascript/#javascript-on-nodejs
[3]流关闭事件: https://nodejs.org/api/stream.html#stream_event_close
[4]http关闭事件: https://nodejs.org/api/http.html#http_event_close




.



本文分享自微信公众号 - 前端先锋(jingchengyideng)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。