0

0

深入解析 Node.js 的回调队列

青灯夜游

青灯夜游

发布时间:2020-09-01 10:39:06

|

1865人浏览过

|

来源于segmentfault

转载

深入解析 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> 一起运行。

Magic AI Avatars
Magic AI Avatars

神奇的AI头像,获得200多个由AI制作的自定义头像。

下载

以下代码说明了 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)

此队列存储与关闭事件操作关联的函数。

包括以下内容:

这些队列被认为是优先级最低的,因为此处的操作会在以后发生。

你肯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 是单线程的)。
  • 同样在这段时间内,setTimeoutfs.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(非阻塞),这意味着异步操作可以被正确的处理。都是因为有了事件循环和回调队列才能使此功能生效。

更多编程相关知识,可访问:编程教学!!

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
node.js调试
node.js调试

node.js调试可以使用console.log()输出调试信息、断点调试和第三方调试工具。详细介绍:1、console.log()输出调试信息,通过在代码中插入console.log()语句,开发人员可以在控制台输出变量的值、函数的执行结果等信息,以便观察代码的执行流程和状态;2、断点调试,可以在代码中设置断点,以便在特定位置暂停代码的执行,观察变量的值和执行流程等。

362

2023.09.19

JavaScript 全栈开发基础(Node.js + 前端)
JavaScript 全栈开发基础(Node.js + 前端)

本专题系统介绍 JavaScript 在全栈开发中的核心知识结构,涵盖 Node.js 基础、Express/Koa 接口构建、前端交互设计、模块化与包管理、数据库连接、前后端数据通信与部署流程。通过完整项目示例,帮助学习者掌握从浏览器到服务器的一体化开发能力,实现真正意义上的全栈入门。

118

2025.11.26

Node.js后端开发与Express框架实践
Node.js后端开发与Express框架实践

本专题针对初中级 Node.js 开发者,系统讲解如何使用 Express 框架搭建高性能后端服务。内容包括路由设计、中间件开发、数据库集成、API 安全与异常处理,以及 RESTful API 的设计与优化。通过实际项目演示,帮助开发者快速掌握 Node.js 后端开发流程。

419

2026.02.10

while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

107

2023.09.25

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

549

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

30

2025.12.22

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

44

2026.01.06

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

443

2023.07.18

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Node.js 教程
Node.js 教程

共57课时 | 13.2万人学习

【web前端】Node.js快速入门
【web前端】Node.js快速入门

共16课时 | 2.1万人学习

Node.js-前端工程化必学
Node.js-前端工程化必学

共19课时 | 3.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号