理解 Node.js 中的事件循环
你已经使用 Node.js 一段时间了,构建了一些应用程序,尝试了不同的模块,甚至对异步编程感到很舒适。但是有些事情一直在困扰着你——事件循环(Event Loop)。
如果你像我一样,花费了无数个小时阅读文档和观看视频,试图理解事件循环。但即使作为一个经验丰富的开发者,在完全理解它如何工作方面也可能会遇到困难。这就是为什么我准备了这份视觉指南,帮助您充分理解 Node.js 事件循环。请坐下来,拿杯咖啡,让我们深入探索 Node.js 事件循环的世界吧。
JavaScript 中的异步编程
我们将从 JavaScript 中异步编程的复习开始。虽然 JavaScript 在 Web、移动和桌面应用程序中都有使用,但重要的是要记住,本质上,JavaScript 是一种同步、阻塞、单线程的语言。让我们通过一个简短的代码片段来理解这句话。
// index.jsfunction 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 代码都比异步代码优先级更高。这表示只有在调用堆栈为空时,事件循环才会发挥作用。
在事件循环中,执行顺序遵循某些规则。需要掌握的规则还是有一些的,我们逐个的了解一下:
- 执行微任务队列(microtask queue)中的所有回调函数。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务。
- 执行计时器队列(timer queue)内的所有回调函数。
- 如果微任务队列中存在回调函数,则在计时器队列内每执行完一次回调函数之后执行微任务队列中的所有回调函数。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务。
- 执行 I/O 队列(I/O queue)内的所有回调函数。
- 如果微任务队列中存在回调函数,按照先 nextTick 队列后 Promise 队列的顺序依次执行微任务队列中的所有回调函数。
- 执行检查队列(check queue)内的所有回调函数。
- 如果微任务队列中存在回调函数,则在检查队列内每个回调之后执行微任务队列中的所有回调函数 。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务。
- 执行关闭队列(close queue)内的所有回调函数。
- 在同一循环的最后,再执行一次微任务队列。首先是 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/
相关文章:

理解 Node.js 中的事件循环
你已经使用 Node.js 一段时间了,构建了一些应用程序,尝试了不同的模块,甚至对异步编程感到很舒适。但是有些事情一直在困扰着你——事件循环(Event Loop)。 如果你像我一样,花费了无数个小时阅读文档和观看…...

Mac 软件出现「意外退出」及「打不开」解决方法
Mac 软件出现「意外退出」及「打不开」解决方法 软件出现意外退出及软件损坏的情况,这是因为苹果删除了TNT的证书,所以大部分TNT破解的Mac软件会出现无法打开,提示意外退出。 终端需先安装Xcode或Apple命令行工具 如未装Xcode可以使用下列命…...

随机森林 3(代码)
通过随机森林 1和随机森林 2 的介绍,相信大家对理论已经了解的很透彻,接下来带大家敲一下代码,不懂得可以加我入群讨论。 第一份代码是比较原始的代码,第二份代码是第一段代码中引用的primitive_plot,第三份代码是使用…...

勒索事件急剧增长,亚信安全发布《勒索家族和勒索事件监控报告》
近期(12.15-12.21)态势快速感知 近期全球共发生了247起攻击和勒索事件,勒索事件数量急剧增长。 近期需要重点关注的除了仍然流行的勒索家族lockbit3以外,还有本周top1勒索组织toufan。toufan是一个新兴勒索组织,本周共发起了108起勒索攻击&a…...
LeetCode1523. Count Odd Numbers in an Interval Range
文章目录 一、题目二、题解 一、题目 Given two non-negative integers low and high. Return the count of odd numbers between low and high (inclusive). Example 1: Input: low 3, high 7 Output: 3 Explanation: The odd numbers between 3 and 7 are [3,5,7]. Exam…...
E中国铜金属行业需求前景及未来发展机遇分析报告2024-2030年
E中国铜金属行业需求前景及未来发展机遇分析报告2024-2030年 &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& 《报告编号》: BG471816 《出…...
python SVM 保存和加载模型参数
在 Python 中,你可以使用 scikit-learn 库中的 joblib 或 pickle 模块来保存和加载 SVM 模型的参数。以下是一个简单的示例代码,演示了如何使用 joblib 模块保存和加载 SVM 模型的参数: 保存模型参数: from sklearn import svm …...
JAVA进化史: JDK12特性及说明
JDK 12于2019年3月发布。这个版本相对于之前的版本来说规模较小,主要集中在一些改进和实验性的特性上。以下是JDK 12的一些主要特性: 引入了实验性的Shenandoah垃圾收集器 JDK 12引入了实验性的Shenandoah垃圾收集器,旨在实现极低的暂停时间…...

Databend 的算力可扩展性
作者:尚卓燃(PsiACE) 澳门科技大学在读硕士,Databend 研发工程师实习生 Apache OpenDAL(Incubating) Committer PsiACE (Chojan Shang) GitHub 对于大规模分布式数据处理系统,为了更好应对数据、流量、和复杂性的增长…...

「解析」Windows 如何优雅使用 Terminal
所谓工欲善其事必先利其器,对于开发人员 Linux可能是首选,但是在家学习的时候,我还是更喜欢使用 Windows系统,首先是稳定,其次是习惯了。当然了,我还有一台专门安装 Linux系统的小主机用于学习Linux使用&am…...

Linux第18步_安装“Ubuntu系统下的C语言编译器GCC”
Ubuntu系统没有提供C/C的编译环境,因此还需要手动安装build-essential软件包,它包含了 GNU 编辑器,GNU 调试器,和其他编译软件所必需的开发库和工具。本节用于重点介绍安装“Ubuntu系统下的C语言编译器GC&a…...
【Linux】Linux 基础命令 crontab命令
1.crontab命令 crond 是linux下用来周期性的执行某种任务或等待处理某些事件的一个守护进程,与windows下的计划任务类似,当安装完成操作系统后,默认会安装此服务 工具,并且会自动启动crond进程,crond进程每分钟会定期检查是否有要执行的任务,如果有要执行的任务,则自动…...

14:00面试,14:08就出来了,问的问题过于变态了。。。
从小厂出来,没想到在另一家公司又寄了。 到这家公司开始上班,加班是每天必不可少的,看在钱给的比较多的份上,就不太计较了。没想到10月一纸通知,所有人不准加班,加班费不仅没有了,薪资还要降40…...
Ubuntu envs setting
1. change the chmod of folders sudo chown -R $USER:$USER /home/anaconda3 2. torch.cuda.is_available()返回false change conda installation to pip. zai qi ta huan jing pei zhi dou mei wen ti de qing kuang xia , zai shi shi zhe ge fang fa. # CUDA 11.7 con…...

Windows 下用 C++ 调用 Python
文章目录 Part.I IntroductionChap.I InformationChap.II 预备知识 Part.II 语法Chap.I PyRun_SimpleStringChap.II C / Python 变量之间的相互转换 Part.III 实例Chap.I 文件内容Chap.II 基于 Visual Studio IDEChap.III 基于 cmakeChap.IV 运行结果 Part.IV 可能出现的问题Ch…...

九州金榜|家庭教育一招孩子不在任性
有一次和朋友一块聚餐,邻座是一位妈妈、和她大概七八岁的儿子,小男孩长得很帅气,没有像同龄人那样调皮捣乱,而是和妈妈很温馨的就餐。 看的出来一家人的素质很高,就餐过程中桌面保持的很整洁,交流声音也不…...
爬虫案列 --抖音视频批量爬取
""" 项目名称: 唯品会商品数据爬取 项目描述: 通过requests框架获取网页数据 项目环境: pycharm && python3.8 作者所属: 几许1. 对主页抓包 , 鼠标移动到视频位置视频自动播放获得视频数据包 2. 对视频数据包地址进行解析 , 复制链接 , 进行检索 3. 获…...

【React系列】React中的CSS
本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNA&actiongetalbum&album_id1566025152667107329) 一. React中的css方案 1.1. react 中的 css 事实上,css 一直是 React 的痛点,也是被很多开发…...

基于Kettle开发的web版数据集成开源工具(data-integration)-应用篇
目录 📚第一章 基本流程梳理📗页面基本操作📗对应后台服务流程 📚第二章 二开思路📗前端📗后端 🔼上一集:基于Kettle开发的web版数据集成开源工具(data-integration)-介绍篇 *️⃣主…...
51单片机三种编译模式的相互关系
51单片机三种编译模式的相互关系 编译模式默认存储类型RAM使用规模变量使用特点SAMLLdata128B片内RAM使用规模CPU访问数据速度快,但存储容量较小COMPACTpdata258B片外分页RAM速度和容量介于上下两者之间LARGExdata64KB片外RAMCPU访问数据的速度较慢,但存…...
浅谈 React Hooks
React Hooks 是 React 16.8 引入的一组 API,用于在函数组件中使用 state 和其他 React 特性(例如生命周期方法、context 等)。Hooks 通过简洁的函数接口,解决了状态与 UI 的高度解耦,通过函数式编程范式实现更灵活 Rea…...

Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...

Appium+python自动化(十六)- ADB命令
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试…...
三维GIS开发cesium智慧地铁教程(5)Cesium相机控制
一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点: 路径验证:确保相对路径.…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI
前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。 如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LA…...
linux 下常用变更-8
1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行,YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID: YW3…...

ios苹果系统,js 滑动屏幕、锚定无效
现象:window.addEventListener监听touch无效,划不动屏幕,但是代码逻辑都有执行到。 scrollIntoView也无效。 原因:这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作,从而会影响…...

【Oracle】分区表
个人主页:Guiat 归属专栏:Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...

嵌入式学习笔记DAY33(网络编程——TCP)
一、网络架构 C/S (client/server 客户端/服务器):由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序,负责提供用户界面和交互逻辑 ,接收用户输入,向服务器发送请求,并展示服务…...