理解 Node.js 中的事件循环

news2024/11/19 20:17:29

你已经使用 Node.js 一段时间了,构建了一些应用程序,尝试了不同的模块,甚至对异步编程感到很舒适。但是有些事情一直在困扰着你——事件循环(Event Loop)。

如果你像我一样,花费了无数个小时阅读文档和观看视频,试图理解事件循环。但即使作为一个经验丰富的开发者,在完全理解它如何工作方面也可能会遇到困难。这就是为什么我准备了这份视觉指南,帮助您充分理解 Node.js 事件循环。请坐下来,拿杯咖啡,让我们深入探索 Node.js 事件循环的世界吧。

JavaScript 中的异步编程

我们将从 JavaScript 中异步编程的复习开始。虽然 JavaScript 在 Web、移动和桌面应用程序中都有使用,但重要的是要记住,本质上,JavaScript 是一种同步、阻塞、单线程的语言。让我们通过一个简短的代码片段来理解这句话。

// index.js

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

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

A()
B()

// Logs A and then B

JavaScript 是同步的

如果我们有两个将消息记录到控制台的函数,那么代码会自上而下执行,每次只执行一行。在上述代码片段中,我们看到 A 在 B 之前被记录。

JavaScript 是阻塞的

JavaScript 由于其同步性质而被阻塞。无论前一个进程需要多长时间,后续进程都不会启动,直到前者完成为止。在代码片段中,如果函数 A 必须执行大量代码块,则 JavaScript 必须在没有转移到函数 B 的情况下完成该操作。即便这块代码需要耗时 10 秒甚至 1 分钟。

你可能已经在浏览器中遇到过这种情况。当 Web 应用程序在浏览器中运行并且执行一些密集的代码块而不返回控制权给浏览器时,浏览器可能会出现卡死的情况,这就是所谓的阻塞。浏览器被阻止继续处理用户输入和执行其他任务,直到 Web 应用程序将处理器控制权归还给浏览器。

JavaScript 是单线程的

线程就是你的 JavaScript 程序可以用来运行任务的进程(process)。每个线程一次只能执行一个任务。与其他支持多线程并且可以同时运行多个任务的语言不同,JavaScript 只有一个称为主线程的线程执行代码。

等待 JavaScript

如你所想,这种 JavaScript 模型会带来问题,因为我们必须等待数据被获取后才能继续执行代码。这个等待可能需要几秒钟,在此期间我们无法运行任何其他代码。如果 JavaScript 在不等待的情况下继续处理,就会出错。我们需要在 JavaScript 中实现异步行为。我们进到 Node.js 看一下。

Node.js 运行时

图片

Node.js 运行时是一个环境,你可以在不使用浏览器的情况下使用和运行 JavaScript 程序。核心——Node 运行时,由三个主要组件组成。

  • 外部依赖项 —— 例如 V8、libuv、crypto 等——是 Node.js 必需的功能
  • C++ 特性提供了文件系统访问和网络等功能。
  • JavaScript 库提供了函数和工具,便于使用 JavaScript 代码调用 C++ 特性。

虽然所有部分都很重要,但异步编程在 Node.js 中的关键组件是 libuv。

Libuv

Libuv[2] 是一个跨平台的开源库,用 C 语言编写。在 Node.js 运行时中,它的作用是提供处理异步操作的支持。我们来看一下它是如何工作的。

Node.js 运行时中的代码执行

图片

让我们来概括一下代码在 Node 运行时中的执行方式。在执行代码时,位于图片左侧的 V8 引擎负责 JavaScript 代码的执行。该引擎包含一个内存堆(Memory heap)和一个调用栈(Call stack)。

每当声明变量或函数时,都会在堆上分配内存。执行代码时,函数就会被推入调用栈中。当函数返回时,它就从调用栈中弹出了。这是对栈数据结构的简单实现,最后添加的项是第一个被移除。在图片右侧,是负责处理异步方法的 libuv。

每当我们执行异步方法时,libuv 接管任务的执行。然后使用操作系统本地异步机制运行任务。如果本地机制不可用或不足,则利用其线程池来运行任务,并确保主线程不被阻塞。

同步代码执行

首先,让我们来看一下同步代码执行。以下代码由三个控制台日志语句组成,依次记录“First”,“Second”和“Third”。我们按照运行时执行顺序来查看代码。

// index.js
console.log("First");
console.log("Second");
console.log("Third");

以下是 Node 运行时执行同步代码的可视化展示。

图片

图片

执行的主线程始终从全局作用域开始。全局函数(如果我们可以这样称呼它)被推入堆栈中。然后,在第 1 行,我们有一个控制台日志语句。这个函数被推入堆栈中。假设这个发生在 1 毫秒时,“First” 被记录在控制台上。然后,这个函数从堆栈中弹出。

执行到第 2 行时。假设到第 2 毫秒了,log 函数再次被推入堆栈中。“Second”被记录在控制台上,并弹出该函数。

最后,执行到第 3 行了。第 3 毫秒时,log 函数被推入堆栈,“Third”将记录在控制台上,并弹出该函数。此时已经没有代码要执行,全局也被弹出。

异步代码执行

接下来,让我们看一下异步代码执行。有以下代码片段:包含三个日志语句,但这次第二个日志语句传递给了fs.readFile() 作为回调函数。

图片

图片

执行的主线程始终从全局作用域开始。全局函数被推入堆栈。然后执行到第 1 行,在第 1 毫秒时,“First”被记录在控制台中,并弹出该函数。然后执行移动到第 2 行,在第 2毫秒时,readFile 方法被推入堆栈。由于 readFile 是异步操作,因此它会转移(off-loaded)到 libuv。

JavaScript 从调用堆栈中弹出了 readFile 方法,因为就第 2 行的执行而言,它的工作已经完成了。在后台,libuv 开始在单独的线程上读取文件内容。在第 3 毫秒时,JavaScript 继续进行到第 5 行,将 log 函数推入堆栈,“Third”被记录到控制台中,并将该函数弹出堆栈。

大约在第 4 毫秒左右,假设文件读取任务已经完成,则相关回调函数现在会在调用栈上执行, 在回调函数内部遇到 log 函数。

log 函数推入到到调用栈,“Second”被记录到控制台并弹出 log 函数 。由于回调函数中没有更多要执行的语句,因此也被弹出 。没有更多代码可运行了 ,所以全局函数也从堆栈中删除 。

控制台输出“First”,“Third”,然后是“Second”。

Libuv 和异步操作

很明显,libuv 用于处理 Node.js 中的异步操作。对于像处理网络请求这样的异步操作,libuv 依赖于操作系统原生机制。对于没有本地 OS 支持的异步读取文件的操作,libuv 则依赖其线程池以确保主线程不被阻塞。然而,这也引发了一些问题。

  • 当一个异步任务在 libuv 中完成时,什么时候 Node 会在调用栈上运行相关联的回调函数?
  • Node 是否会等待调用栈为空后再运行回调函数?还是打断正常执行流来运行回调函数?
  • 像 setTimeout 和 setInterval 这类延迟执行回调函数的方法又是何时执行回调函数呢?
  • 如果 setTimeout 和 readFile 这类异步任务同时完成,Node 如何决定哪个回调函数先在调用栈上运行?其中一个会有更多的优先级吗?

所有这些问题都可以通过理解 libuv 核心部分——事件循环来得到答案。

什么是事件循环?

从技术上讲,事件循环只是一个 C 语言程序。但是在 Node.js 中,你可以将其视为一种设计模式,用于协调同步和异步代码的执行。

可视化事件循环

事件循环是一个循环,只要你的 Node.js 应用程序在运行,它就一直运行。每个循环中有六个不同的队列,每个队列都包含一个或多个需要最终在调用堆栈上执行的回调函数。

图片

  • 首先,有一个计时器队列(timer queue。技术上叫最小堆(min-heap)),它保存与 setTimeout 和 setInterval 相关的回调函数。
  • 其次,有一个 I/O 队列(I/O queue),其中包含与所有异步方法相关的回调函数,例如 fs 和 http 模块中提供的相关方法。
  • 第三个是检查队列(check queue),它保存与 setImmediate 函数相关的回调函数,这是特定于Node 的功能。
  • 第四个是关闭队列(close queue),它保存与异步任务关闭事件相关联的回调函数。

最后,有两个不同队列组成微任务队列(microtask queue)。

  • nextTick 队列保存了与 process.nextTick 函数关联的回调函数。
  • Promise 队列则保存了JavaScript 中本地 Promise 相关联的回调函数。

需要注意的是计时器、I/O、检查和关闭队列都属于 libuv。然而,两个微任务队列并不属于 libuv。尽管如此,它们仍然是 Node 运行时环境中扮演着重要角色,并且在执行回调顺序方面发挥着重要作用。说到这里, 让我们来理解一下事件循环是如何工作的。

事件循环是如何工作的?

图中箭头是一个提示,但可能还不太容易理解。让我来解释一下队列的优先级顺序。首先要知道,所有用户编写的同步 JavaScript 代码都比异步代码优先级更高。这表示只有在调用堆栈为空时,事件循环才会发挥作用。

在事件循环中,执行顺序遵循某些规则。需要掌握的规则还是有一些的,我们逐个的了解一下:

  1. 执行微任务队列(microtask queue)中的所有回调函数。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务。
  2. 执行计时器队列(timer queue)内的所有回调函数。
  3. 如果微任务队列中存在回调函数,则在计时器队列内每执行完一次回调函数之后执行微任务队列中的所有回调函数。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务。
  4. 执行 I/O 队列(I/O queue)内的所有回调函数。
  5. 如果微任务队列中存在回调函数,按照先 nextTick 队列后 Promise 队列的顺序依次执行微任务队列中的所有回调函数。
  6. 执行检查队列(check queue)内的所有回调函数。
  7. 如果微任务队列中存在回调函数,则在检查队列内每个回调之后执行微任务队列中的所有回调函数 。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务。
  8. 执行关闭队列(close queue)内的所有回调函数。
  9. 在同一循环的最后,再执行一次微任务队列。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务。

此时,如果还有更多的回调需要处理,那么事件循环再运行一次(译注:事件循环在程序运行期间一直在运行,在当前没有可供处理的任务情况下,会处于等待状态,一旦有新任务就会执行),并重复相同的步骤。另一方面,如果所有回调都已执行并且没有更多代码要处理(译注:也就是程序执行结束),则事件循环退出。

这就是 libuv 事件循环在 Node.js 中执行异步代码的作用。有了这些规则,我们可以重新审视之前提出的问题。

当一个异步任务在 libuv 中完成时,什么时候 Node 会在调用栈上运行相关联的回调函数?

答案:只有当调用栈为空时才执行回调函数。

Node 是否会等待调用栈为空后再运行回调函数?还是打断正常执行流来运行回调函数?

答案:运行回调函数时不会打断正常执行流。

像 setTimeout 和 setInterval 这类延迟执行回调函数的方法又是何时执行回调函数呢?

答案:*setTimeout 和 setInterval 的所有回调函数中第一优先级执行的(不考虑微任务队列)。*

如果两个异步任务(例如 setTimeout 和 readFile)同时完成,Node 如何决定那个回调函数先在调用栈中执行?其中一个会比另一个有更高优先权吗?

答案:在同时完成的情况下,计时器回调会先于 I/O 回调执行。

到此为止我们学了很多,但我希望大家可以把下面这张图片展现的执行顺序铭记于心,因为它完整的表现了 Node.js 在幕后是如何执行异步代码的。

图片

结论

事件循环是 Node.js 的基本组成部分,通过确保主线程不被阻塞来实现异步编程。了解事件循环的工作原理可能具有挑战性,但对于构建高效应用程序至关重要。

这个视觉指南涵盖了 JavaScript 中异步编程、Node.js 运行时和负责处理异步操作的 libuv 的基础知识。有了这些知识,你可以建立一个强大的事件循环模型,在编写利用 Node.js 异步特性的代码时受益。

参考资料

[1]A Complete Visual Guide to Understanding the Node.js Event Loop:https://www.builder.io/blog/visual-guide-to-nodejs-event-loop

[2]Libuv:https://libuv.org/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1358591.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

将音频与视频格式互转的7 个顶级工具方法

你是否遇到过需要将视频文件从一种格式转换为另一种格式的情况? 在编辑家庭电影或者专业电影工作室工作,我们经常需要将视频文件转换成不同的格式。市场上有很多自称能够高效转换的工具,但是我们要时刻警惕诈骗工具,它们可能会耗…

【mac-m1 docker 安装upload-labs靶场】

1.搜索upload-labs docker search upload-labs 2.下载upload-labs docker pull c0ny1/upload-labs 3.启动 docker run -it -d --name uploadlabs -p 80:80 c0ny1/upload-labs --platform linux/amd64 4.访问127.0.0.1:80 注意点:后续使用的时候会报错 需要手动创…

BAT036:TXT与DOC格式互转、DOC与DOCX格式互转

引言:编写批处理程序,可实现txt与doc文档格式互转、doc与docx文档格式互转。 一、新建Windows批处理文件 参考博客: BAT002:在右键菜单新建中添加【Windows批处理文件】_为右键菜单添加bat-CSDN博客 二、TXT与DOC格式互转 1.右键新建的批处理文件,点击【编辑】。 ​ …

虚幻UE 增强输入-触发器

上一篇增强输入基础:虚幻UE 增强输入-第三人称模板增强输入分析与扩展 主要对第三人称模板的增强输入进行分析、复刻和扩展 本篇将会对增强输入中的触发器中的各参数进行讲解 文章目录 前言触发器参数1、下移TriggerDown2、已按下TriggerPressed3、已松开TriggerRel…

新手深入浅出理解PyTorch归一化层全解析

目录 torch.nn子模块normal层详解 nn.BatchNorm1d BatchNorm1d 函数简介 函数工作原理 参数详解 使用技巧与注意事项 示例代码 nn.BatchNorm2d BatchNorm2d 函数简介 函数工作原理 参数详解 使用技巧与注意事项 示例代码 nn.BatchNorm3d BatchNorm3d 函数简介 参…

为什么在国内考CISP比CISSP好?

在国内考CISP比CISSP好的原因主要有以下几点👇 1️⃣国内认证认可度高 💎CISP是国内信息an全领域的重要认证,得到了国内政fu、企业和行业的高度认可。 2️⃣国内考试难度相对较低 由于CISP的考试内容与国内信息an全领域的实际情况更加贴近&am…

MySQL 8.0.32 双写参数和innodb_redo

版本为mysql 8.0.32 数据库内存和磁盘架构 #ib_16384_0.dblwr #ib_16384_0.dblwr和#ib_16384_2.dblwr 这两个文件有什么区别 从架构图中,不难看出这两个文件是双写buffer文件。 双写缓冲区是一个存储区域,在 InnoDB将页面写入 InnoDB数据文件中的正确…

C#上位机与欧姆龙PLC的通信10----开发专用的通讯工具软件(WPF版)

1、介绍 上节开发了一个winform版的通讯测试工具,这节再搞个wpf版的,wpf是什么?请自行百度,也可以看前面的博客,WPF真入门教程,wpf的界面效果是比winform漂亮,因为wpf使用了web项目中的css样式…

使用qtcreator创建qml项目(图解)

接下来就一直点继续,最后完成项目,如下图。 下面对项目进行一些基本的描述 (1)qt项目文件使用pro后缀,是qt项目的配置文件,它用于指定项目的各种参数,包括源文件、头文件、库依赖、编译选项等&a…

全网唯一值得推荐的C/C++框架和库

全网唯一值得推荐的C/C框架和库 C程序员开发指南 ​ 关注我,天天分享C/C开发技术干货! ​关注他 30 人赞同了该文章 ​ 目录 收起 标准库 C通用框架和库 人工智能 异步事件循环 音频 生态学 压缩 并发性 容器 数据库 调试 游戏引擎 图…

C++程序设计兼谈对象模型(侯捷)笔记

C程序设计兼谈对象模型(侯捷) 这是C面向对象程序设计的续集笔记,仅供个人学习使用。如有侵权,请联系删除。 主要内容:涉及到模板中的类模板、函数模板、成员模板以及模板模板参数,后面包含对象模型中虚函数调用&…

【合阳新起点公益】“关爱留守儿童 守护牙齿健康”牙膏发放活动

为了关爱儿童的口腔健康,帮助他们改善生活状况,养成良好的刷牙习惯。合阳县未成年人保护中心、合阳县新起点公益服务中心组织链接到汕头市惠泽人志愿服务中心,为孩子们申请到一批儿童爱心牙膏套盒,分别于2023年12月22日、12月30日…

基于seatunnel实现mysql同步clickhouse验证

场景: 需求想要实现mysql同步到clickhouse,seatunnel部署见前面文档linux环境seatunnel安装运行-CSDN博客。 官方说明文档 Clickhouse | Apache SeaTunnel mysql同步配置 server-id1 log_bin/var/lib/mysql/bin.log binlog_formatROW #binlog-do-db 具…

必看!干得不能再干了,Abaqus各版本安装指南

Abaqus下载链接 https://pan.baidu.com/s/1PHWhlM4cPINUqy2XYRHrhQ?pwd0531 ​1.鼠标右击【Abaqus2023(64bit)】压缩包(win11及以上系统需先点击“显示更多选项”)【解压到 Abaqus.2023(64bit)】。 2.打开解压后的文件夹,鼠标右击【jdk-17…

2024年MySQL学习指南(四),探索MySQL数据库,掌握未来数据管理趋势

文章目录 前言9. 约束的概念10. 约束的分类11. 非空约束12. 唯一约束13. 主键约束14. 默认约束15. 外键约束16. 约束的案例练习 前言 接上篇: 2024年MySQL学习指南(一) 2024年MySQL学习指南(二) 2024年MySQL学习指…

影响机器人行业爆炸式发展的原因有哪些(OpenPilot)

机器人是现在和未来最有发展前景的行业,这一点是毋庸置疑的。 先看一段21年的视频: 外置式L2级别自动驾驶辅助套件 Openpilot 深度体验分析 火热 作为热门专业的从业人员,能够感受到行业的关注热度和实际热度之间的差异。 原因 有很多&am…

操作系统(期末复习知识点)

第1章 操作系统引论 操作系统的目标和作用 操作系统的目标 在计算机系统上配置OS,其主要目的是实现:方便性、有效性、可扩充性、开放性。 操作系统的作用 可以从人机交互、资源管理及资源抽象等不同方面来分析OS在计算机系统中所起到的作用。 OS 作为…

怎么测试电源负载瞬态响应?具体步骤是什么?

负载瞬态响应(Load Transient Response)指的是电路或系统在负载发生瞬态变化时,输出电压或电流出现的瞬时变化的情况。 电源负载瞬态响应测试是评估电源在负载变化时性能表现的一种重要方法。在实际应用中,电源负载的变化是不可避免的,因此准确评估电源的…

面试必问究极重点之HashMap的底层原理

1.底层数据结构 JDK版本不同的数据结构 1.7 数组 链表 1.8 数组 (链表 | 红黑树) 2.添加数据put 在添加一个值的时候,首先会计算他的hash码,然后进行二次hash,在对当前长度取模得到在底层数组中的索引位置当取模完…

用Spring Boot 3.2虚拟线程搭建静态文件服务器有多快?

Spring Boot 3.2 于 2023 年 11 月大张旗鼓地发布,标志着 Java 开发领域的一个关键时刻。这一突破性的版本引入了一系列革命性的功能,包括: 虚拟线程:利用 Project Loom 的虚拟线程释放可扩展性,从而减少资源消耗并增…