1. 首页
  2. IT资讯

深入研究 Node.js 的回调队列

深入研究 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-finish-first-handle (先结束先处理)的操作。

之所以不能这样做的一个原因是,在一个异步操作中可能还会包含另一个异步操作。

为第一个异步过程留出空间意味着必须先要完成内部异步过程,然后才能考虑队列中的其他异步操作。

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

让我们简要地看一下 Node.js 是怎样处理异步操作的。

调用栈,事件循环和回调队列

调用栈被用于跟踪当前正在执行的函数以及从何处开始运行。当一个函数将要执行时,它会被添加到调用堆栈中。这有助于 JavaScript 在执行函数后重新跟踪其处理步骤。

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

请注意,Node.js 负责所有异步活动,因为 JavaScript 可以利用其单线程性质来阻止产生新的线程。

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

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

首先,让我们看一下回调队列的五种主要类型。

回调队列的类型

IO 队列(IO queue)

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

JavaScript 无法访问计算机的内部设备。当执行此类操作时,JavaScript 会将其传输到 Node.js 以在后台处理。

完成后,它们将会被转移到 IO 回调队列中,来进行事件循环,以转移到调用栈中执行。

计时器队列(Timer queue)

每个涉及 Node.js 计时器功能的操作(如 setTimeout()setInterval())都是要被添加到计时器队列的。

请注意,JavaScript 语言本身没有计时器功能。它使用 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> }