javascript的回调函数-深入了解 Node.js 的跳出队列

转发链接:

前言

队列是 Node.js 中用于高效处理异步操作的一项重要技术。 在本文中,我们将深入了解 Node.js 中的队列:它们是什么、它们如何工作(通过风暴循环)以及它们的类型。

Node.js 中的队列是什么?

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

在 Node.js 中处理异步操作可能具有挑战性。

根据网络质量,HTTP 请求期间可能会出现不可预测的延迟(或更糟糕的是没有结果)。 根据文件的大小,尝试使用 Node.js 读写文件时也可能会出现延迟。

与计时器和许多其他操作一样,异步操作完成的时间可能是不确定的。

Node.js 需要能够在各种延迟的情况下有效地处理所有这些。

Node.js 无法处理基于first-start-first-handle (first-start-first-handle) 或first-finish-first-handle (first-finish-first-handle) 的操作。

不能这样做的原因之一是一个异步操作可能包含另一个异步操作。

为第一个异步进程腾出空间意味着内部异步进程必须在考虑队列中的其他异步操作之前完成。

有很多情况需要考虑,因此最好的选择是制定规则。 此规则影响风暴循环和队列在 Node.js 中的工作方式。

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

调用堆栈、事件循环和反弹队列

调用堆栈用于跟踪当前正在执行的函数及其开始位置。 当函数即将执行时,它会被添加到调用堆栈中。 这有助于 JavaScript 在执行函数后回溯其处理步骤。

回调队列是当后台操作完成时将回调函数作为异步操作保存的队列。 它们以先进先出 (FIFO) 方式工作。 我们将在本文前面介绍不同类型的退回队列。

请注意,Node.js 负责所有异步活动,因为 JavaScript 可以利用其单线程特性来防止形成新线程。

它还负责在后台操作完成后将函数添加到退回队列。 JavaScript 本身与退回队列无关。 同时,storm循环会不断检查调用栈是否为空,以便可以从反弹队列中提取一个函数并添加到调用栈中。 事件循环仅在执行所有同步操作后才检查队列。

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

首先,我们来看看退回队列的五种主要类型。

回调队列的类型IO队列(IO队列)

IO操作是指涉及外部设备(如电脑硬盘、网卡等)的操作。 常见的操作包括读写文件操作、网络操作等。这些操作应该是异步的,因为它们交给 Node.js 来处理。

JavaScript 无法访问您计算机的内部设备。 完成后,JavaScript 将其传输到 Node.js 在后台进行处理。

完成后,它们将被转移到IO回调队列中进行风暴循环,以转移到调用堆栈中执行。

定时器队列

每个涉及 Node.js 计时器函数 [1](例如 setTimeout() 和 setInterval())的操作都会添加到计时器队列中。

请注意,JavaScript 语言本身没有计时器功能 [2]。 它使用 Node.js 提供的计时器 API(包括 setTimeout)来执行与时间相关的操作。 所以定时器操作是异步的。 无论是 2 秒还是 0 秒,JavaScript 都会将与时间相关的操作交给 Node.js,然后 Node.js 将完成并添加到计时器队列中。

例如:

setTimeout(function() {
        console.log('setTimeout');
    }, 0)
    console.log('yeah')
# 返回
yeah
setTimeout

在处理异步操作时,JavaScript 会继续执行其他操作。 事件循环只有在处理完所有同步操作后才能进入反弹队列。

微任务队列

队列分为两个队列:

事件循环执行的每次迭代称为一个tick。

process.nextTick 是一个在下一个时钟周期(即风暴循环的下一次迭代)执行函数的函数。 微任务队列需要存储这些函数,以便它们可以在下一个tick上执行。

这意味着风暴循环在进入其他队列之前必须继续检查微任务队列中的此类函数。

可以看到,在IO和定时器队列中,所有与异步操作相关的事情都交给了异步函数。

但 Promise 是不同的。在 Promise 中,初始变量存储在 JavaScript 内存中(你可能已经注意到

)。

异步操作完成后,Node.js 将函数(附加到 Promise)放入微任务队列中。同时,它用结果更新 JavaScript 内存中的变量,以便该函数不会与

一起跑。

以下代码说明了 Promise 的工作原理:

let prom = new Promise(function (resolve, reject) {
        // 延迟执行
        setTimeout(function () {
            return resolve("hello");
        }, 2000);
    });
    console.log(prom);
    // Promise {  }
    
    prom.then(function (response) {
        console.log(response);
    });
    // 在 2000ms 之后,输出
    // hello

关于微任务队列,需要注意的是,事件循环在进入其他队列之前会重复检测并执行微任务队列中的函数。 例如,当微任务队列完成或计时器操作完成 Promise 时,事件循环将提交该 Promise,然后再继续处理计时器队列中的其他函数。

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

检查队列

检查队列也称为立即队列。 当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.jsjavascript的回调函数,之后程序会继续执行。

do while操作持续3ms。 在此期间,readFile操作完成并被推送到IO队列。 完成此操作后,事件循环将开始检测队列。

虽然检测队列先被填满,但直到IO队列为空时才考虑使用。 所以在setImmediate之前,将readFile输出到控制台。

关闭队列

该队列存储与关闭风暴操作相关的函数。

包括以下这些:

这些队列被认为是最低优先级,因为这里的操作将在稍后发生。

在处理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");

程序流程如下:

在 Node.js 将回调函数添加到 IO 队列之前,fs.readFile 在后台需要 10 毫秒。

当前队列是:

// 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"

当storm循环继续执行队列中的回调函数时,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"

这里需要注意三件事:

总结

JavaScript 是单线程的。 每个异步函数都由 Node.js 处理javascript的回调函数,Node.js 依赖操作系统内部工作。

Node.js 负责将回调函数(通过 JavaScript 附加到异步操作)添加到退回队列。 事件循环确定接下来在每次迭代中执行哪个回调函数。

了解队列在 Node.js 中的工作原理可以让您更好地理解它们,因为队列是环境的核心功能之一。 Node.js 最流行的定义是非阻塞(non-blocking),这意味着异步操作可以被正确处理。 这一切都要感谢 Storm Loop 和 Bounce Queue 来让这个功能发挥作用。