彻底搞懂nodejs事件循环
nodejs是单线程执行的,同时它又是基于事件驱动的非阻塞IO编程模型。这就使得我们不用等待异步操作结果返回,就可以继续往下执行代码。当异步事件触发之后,就会通知主线程,主线程执行相应事件的回调。
以上是众所周知的内容。今天我们从源码入手,分析一下nodejs的事件循环机制。
nodejs架构
首先,我们先看下nodejs架构,下图所示:
如上图所示,nodejs自上而下分为
- 用户代码 ( js 代码 )
用户代码即我们编写的应用程序代码、npm包、nodejs内置的js模块等,我们日常工作中的大部分时间都是编写这个层面的代码。
- binding代码或者三方插件(js 或 C/C++ 代码)
胶水代码,能够让js调用C/C++的代码。可以将其理解为一个桥,桥这头是js,桥那头是C/C++,通过这个桥可以让js调用C/C++。
在nodejs里,胶水代码的主要作用是把nodejs底层实现的C/C++库暴露给js环境。
三方插件是我们自己实现的C/C++库,同时需要我们自己实现胶水代码,将js和C/C++进行桥接。
- 底层库
nodejs的依赖库,包括大名鼎鼎的V8、libuv。
V8: 我们都知道,是google开发的一套高效javascript运行时,nodejs能够高效执行 js 代码的很大原因主要在它。
libuv:是用C语言实现的一套异步功能库,nodejs高效的异步编程模型很大程度上归功于libuv的实现,而libuv则是我们今天重点要分析的。
还有一些其他的依赖库
http-parser:负责解析http响应
openssl:加解密
c-ares:dns解析
npm:nodejs包管理器
…
关于nodejs不再过多介绍,大家可以自行查阅学习,接下来我们重点要分析的就是libuv。
libuv 架构
我们知道,nodejs实现异步机制的核心便是libuv,libuv承担着nodejs与文件、网络等异步任务的沟通桥梁,下面这张图让我们对libuv有个大概的印象:
这是libuv官网的一张图,很明显,nodejs的网络I/O、文件I/O、DNS操作、还有一些用户代码都是在 libuv 工作的。
既然谈到了异步,那么我们首先归纳下nodejs里的异步事件:
- 非I/O:
- 定时器(setTimeout,setInterval)
- microtask(promise)
- process.nextTick
- setImmediate
- DNS.lookup
- I/O:
- 网络I/O
- 文件I/O
- 一些DNS操作
- …
网络I/O
对于网络I/O,各个平台的实现机制不一样,linux 是 epoll 模型,类 unix 是 kquene 、windows 下是高效的 IOCP 完成端口、SunOs 是 event ports,libuv 对这几种网络I/O模型进行了封装。
文件I/O、异步DNS操作
libuv内部还维护着一个默认4个线程的线程池,这些线程负责执行文件I/O操作、DNS操作、用户异步代码。当 js 层传递给 libuv 一个操作任务时,libuv 会把这个任务加到队列中。之后分两种情况:
- 1、线程池中的线程都被占用的时候,队列中任务就要进行排队等待空闲线程。
- 2、线程池中有可用线程时,从队列中取出这个任务执行,执行完毕后,线程归还到线程池,等待下个任务。同时以事件的方式通知event-loop,event-loop接收到事件执行该事件注册的回调函数。
当然,如果觉得4个线程不够用,可以在nodejs启动时,设置环境变量UV_THREADPOOL_SIZE来调整,出于系统性能考虑,libuv 规定可设置线程数不能超过128个。
nodejs源码
先简要介绍下nodejs的启动过程:
- 1、调用platformInit方法 ,初始化 nodejs 的运行环境。
- 2、调用 performance_node_start 方法,对 nodejs 进行性能统计。
- 3、openssl设置的判断。
- 4、调用v8_platform.Initialize,初始化 libuv 线程池。
- 5、调用 V8::Initialize,初始化 V8 环境。
- 6、创建一个nodejs运行实例。
- 7、启动上一步创建好的实例。
- 8、开始执行js文件,同步代码执行完毕后,进入事件循环。
- 9、在没有任何可监听的事件时,销毁 nodejs 实例,程序执行完毕。
以上就是 nodejs 执行一个js文件的全过程。接下来着重介绍第八个步骤,事件循环。
我们看几处关键源码:
- 1、core.c,事件循环运行的核心文件。
int uv_run(uv_loop_t* loop, uv_run_mode mode) {int timeout;int r;int ran_pending;
//判断事件循环是否存活。r = uv__loop_alive(loop);//如果没有存活,更新时间戳if (!r)uv__update_time(loop);
//如果事件循环存活,并且事件循环没有停止。while (r != 0 && loop->stop_flag == 0) {//更新当前时间戳uv__update_time(loop);//执行 timers 队列uv__run_timers(loop);//执行由于上个循环未执行完,并被延迟到这个循环的I/O 回调。ran_pending = uv__run_pending(loop); //内部调用,用户不care,忽略uv__run_idle(loop); //内部调用,用户不care,忽略uv__run_prepare(loop); timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)//计算距离下一个timer到来的时间差。timeout = uv_backend_timeout(loop);//进入 轮询 阶段,该阶段轮询I/O事件,有则执行,无则阻塞,直到超出timeout的时间。uv__io_poll(loop, timeout);//进入check阶段,主要执行 setImmediate 回调。uv__run_check(loop);//进行close阶段,主要执行 **关闭** 事件uv__run_closing_handles(loop);if (mode == UV_RUN_ONCE) {//更新当前时间戳uv__update_time(loop);//再次执行timers回调。uv__run_timers(loop);}//判断当前事件循环是否存活。r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)break;}/* The if statement lets gcc compile it to a conditional store. Avoids * dirtying a cache line. */if (loop->stop_flag != 0)loop->stop_flag = 0;return r;
}
- 2、timers 阶段,源码文件:timers.c。
void uv__run_timers(uv_loop_t* loop) {struct heap_node* heap_node;uv_timer_t* handle;for (;;) {//取出定时器堆中超时时间最近的定时器句柄heap_node = heap_min((struct heap*) &loop->timer_heap);if (heap_node == NULL)break;handle = container_of(heap_node, uv_timer_t, heap_node);// 判断最近的一个定时器句柄的超时时间是否大于当前时间,如果大于当前时间,说明还未超时,跳出循环。if (handle->timeout > loop->time)break;// 停止最近的定时器句柄uv_timer_stop(handle);// 判断定时器句柄类型是否是repeat类型,如果是,重新创建一个定时器句柄。uv_timer_again(handle);//执行定时器句柄绑定的回调函数handle->timer_cb(handle);}
}
- 3、 轮询阶段 源码,源码文件:kquene.c
void uv__io_poll(uv_loop_t* loop, int timeout) {/*一连串的变量初始化*///判断是否有事件发生 if (loop->nfds == 0) {//判断观察者队列是否为空,如果为空,则返回assert(QUEUE_EMPTY(&loop->watcher_queue));return;}nevents = 0;// 观察者队列不为空while (!QUEUE_EMPTY(&loop->watcher_queue)) {/* 取出队列头的观察者对象 取出观察者对象感兴趣的事件并监听。 */....省略一些代码w->events = w->pevents;}assert(timeout >= -1);//如果有超时时间,将当前时间赋给base变量base = loop->time;// 本轮执行监听事件的最大数量count = 48; /* Benchmarks suggest this gives the best throughput. *///进入监听循环for (;; nevents = 0) {// 有超时时间的话,初始化specif (timeout != -1) {spec.tv_sec = timeout / 1000;spec.tv_nsec = (timeout % 1000) * 1000000;}if (pset != NULL)pthread_sigmask(SIG_BLOCK, pset, NULL);// 监听内核事件,当有事件到来时,即返回事件的数量。// timeout 为监听的超时时间,超时时间一到即返回。// 我们知道,timeout是传进来得下一个timers到来的时间差,所以,在timeout时间内,event-loop会一直阻塞在此处,直到超时时间到来或者有内核事件触发。nfds = kevent(loop->backend_fd,events,nevents,events,ARRAY_SIZE(events),timeout == -1 ? NULL : &spec);if (pset != NULL)pthread_sigmask(SIG_UNBLOCK, pset, NULL);/* Update loop->time unconditionally. It's tempting to skip the update when * timeout == 0 (i.e. non-blocking poll) but there is no guarantee that the * operating system didn't reschedule our process while in the syscall. */SAVE_ERRNO(uv__update_time(loop));//如果内核没有监听到可用事件,且本次监听有超时时间,则返回。if (nfds == 0) {assert(timeout != -1);return;}if (nfds == -1) {if (errno != EINTR)abort();if (timeout == 0)return;if (timeout == -1)continue;/* Interrupted by a signal. Update timeout and poll again. */goto update_timeout;}。。。//判断事件循环的观察者队列是否为空assert(loop->watchers != NULL);loop->watchers[loop->nwatchers] = (void*) events;loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds;// 循环处理内核返回的事件,执行事件绑定的回调函数for (i = 0; i < nfds; i++) {。。。。}}
参考 前端进阶面试题详细解答
uv__io_poll阶段源码最长,逻辑最为复杂,可以做个概括,如下:
当js层代码注册的事件回调都没有返回的时候,事件循环会阻塞在poll阶段。看到这里,你可能会想了,会永远阻塞在此处吗?
1、首先呢,在poll阶段执行的时候,会传入一个timeout超时时间,该超时时间就是poll阶段的最大阻塞时间。
2、其次呢,在poll阶段,timeout时间未到的时候,如果有事件返回,就执行该事件注册的回调函数。timeout超时时间到了,则退出poll阶段,执行下一个阶段。
所以,我们不用担心事件循环会永远阻塞在poll阶段。
以上就是事件循环的两个核心阶段。限于篇幅,timers阶段的其他源码和setImmediate、process.nextTick的涉及到的源码就不罗列了,感兴趣的童鞋可以看下源码。
最后,总结出事件循环的原理如下,以上你可以不care,记住下面的总结就好了。
事件循环原理
- node 的初始化
- 初始化 node 环境。
- 执行输入代码。
- 执行 process.nextTick 回调。
- 执行 microtasks。
- 进入 event-loop
- 进入 timers 阶段
- 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerId 升序执行。
- 检查是否有 process.nextTick 任务,如果有,全部执行。
- 检查是否有microtask,如果有,全部执行。
- 退出该阶段。
- 进入IO callbacks阶段。
- 检查是否有 pending 的 I/O 回调。如果有,执行回调。如果没有,退出该阶段。
- 检查是否有 process.nextTick 任务,如果有,全部执行。
- 检查是否有microtask,如果有,全部执行。
- 退出该阶段。
- 进入 idle,prepare 阶段:
- 这两个阶段与我们编程关系不大,暂且按下不表。
- 进入 poll 阶段
- 首先检查是否存在尚未完成的回调,如果存在,那么分两种情况。
- 第一种情况:
- 如果有可用回调(可用回调包含到期的定时器还有一些IO事件等),执行所有可用回调。
- 检查是否有 process.nextTick 回调,如果有,全部执行。
- 检查是否有 microtaks,如果有,全部执行。
- 退出该阶段。
- 第二种情况:
- 如果没有可用回调。
- 检查是否有 immediate 回调,如果有,退出 poll 阶段。如果没有,阻塞在此阶段,等待新的事件通知。
- 第一种情况:
- 如果不存在尚未完成的回调,退出poll阶段。
- 首先检查是否存在尚未完成的回调,如果存在,那么分两种情况。
- 进入 check 阶段。
- 如果有immediate回调,则执行所有immediate回调。
- 检查是否有 process.nextTick 回调,如果有,全部执行。
- 检查是否有 microtaks,如果有,全部执行。
- 退出 check 阶段
- 进入 closing 阶段。
- 如果有immediate回调,则执行所有immediate回调。
- 检查是否有 process.nextTick 回调,如果有,全部执行。
- 检查是否有 microtaks,如果有,全部执行。
- 退出 closing 阶段
- 检查是否有活跃的 handles(定时器、IO等事件句柄)。
- 如果有,继续下一轮循环。
- 如果没有,结束事件循环,退出程序。
- 进入 timers 阶段
细心的童鞋可以发现,在事件循环的每一个子阶段退出之前都会按顺序执行如下过程:
- 检查是否有 process.nextTick 回调,如果有,全部执行。
- 检查是否有 microtaks,如果有,全部执行。
- 退出当前阶段。
记住这个规律哦。
相关文章:

彻底搞懂nodejs事件循环
nodejs是单线程执行的,同时它又是基于事件驱动的非阻塞IO编程模型。这就使得我们不用等待异步操作结果返回,就可以继续往下执行代码。当异步事件触发之后,就会通知主线程,主线程执行相应事件的回调。 以上是众所周知的内容。今天…...

Linux基础命令大全(下)
♥️作者:小刘在C站 ♥️个人主页:小刘主页 ♥️每天分享云计算网络运维课堂笔记,努力不一定有收获,但一定会有收获加油!一起努力,共赴美好人生! ♥️夕阳下,是最美的绽放࿰…...

Matplotlib从入门到精通05-样式色彩秀芳华
Matplotlib从入门到精通05-样式色彩秀芳华总结Matplotlib从入门到精通05-样式色彩秀芳华导入依赖一、matplotlib的绘图样式(style)1.matplotlib预先定义样式2.用户自定义stylesheet3.设置rcparams二、matplotlib的色彩设置(color)…...

< CSS小技巧:那些不常用,却很惊艳的CSS属性 >
文章目录👉 前言👉 一. background-clip: text - 限制背景显示(裁剪)👉 二. user-select - 控制用户能否选中文本👉 三. :focus-within 伪类👉 四. gap - 网格 / 弹性布局间隔设置👉…...

GPT-4 重磅发布,用户直呼:强得离谱
ChatGPT沉寂了一会,OpenAI 的新“核弹”又来了,GPT-4,并且它还非常擅长编码。闲话不提,直捣黄龙。 OpenAI 宣布发布 GPT-4 ChatGPT-4这是 OpenAI 努力扩展深度学习的最新里程碑,GPT-4 是一个大型多模态模型。 据悉&a…...

【JavaSE】知识点总结(3)
目录 一、类定义和使用 1. 类的定义 2. 类的实例化 3. 构造方法 构造方法的重载 二、this关键字 三、 static 修饰属性 四、封装 2. getter与setter 五、继承 1. 继承的语法 2. 子类中访问父类 3. 关于继承原则 4. super关键字 5. super和this 6. protected 关键…...

MySQL基础(三)聚合函数、子查询
目录 聚合函数 AVG/SUM/MAX/MIN COUNT函数 GROUP BY HAVING having和where的区别 SELECT的执行过程 子查询 单行子查询vs多行子查询 单行子查询 多行子查询 关联子查询 EXISTS 与 NOT EXISTS关键字 聚合函数 聚合函数作用于一组数据,并对一组数据返回一个…...

深度学习数据集处理基础内容——xml和json文件详解
文章目录一、xml文件1.1 什么是 XML?1.2XML 和 HTML 之间的差异1.3XML 不会做任何事情1.4通过 XML 您可以发明自己的标签1.5XML 不是对 HTML 的替代1.6XML 无所不在二、json文件基本的JSON结构体类型(共享部分)三、转COCO数据集3.1 info3.2 l…...

蓝桥杯基础技能训练
51单片机系统浓缩图 1. HC138译码器 用3个输入引脚,实现8个输出引脚,而且这个八个输出引脚中只要一个低电平,所以我们只需要记住真值表就行 #include "reg52.h" sbit HC138_A P2^5; sbit HC138_B P2^6; sbit HC…...

【Kubernetes】第二十八篇 - 实现自动构建部署
一,前言 上一篇,介绍了 Deployment、Service 的创建,完成了前端项目的构建部署; 希望实现:推送代码 -> 自动构建部署-> k8s 滚动更新; 本篇,实现自动构建部署 二,推送触发构…...
蓝桥杯刷题第十天
第一题:裁纸刀问题描述本题为填空题,只需要算出结果后,在代码中使用输出语句将所填结果输出即可。小蓝有一个裁纸刀,每次可以将一张纸沿一条直线裁成两半。小蓝用一张纸打印出两行三列共 6 个二维码,至少使用九次裁出来…...

网络安全缓冲区溢出与僵尸网络答题分析
一、缓冲区溢出攻击 缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量,溢出的数据覆盖在合法数据上。理想的情况是:程序会检查数据长度,而且并不允许输入超过缓冲区长度的字符。但是绝大多数程序都会假设数据长度总是…...

机器学习:逻辑回归模型算法原理(附案例实战)
机器学习:逻辑回归模型算法原理 作者:AOAIYI 作者简介:Python领域新星作者、多项比赛获奖者:AOAIYI首页 😊😊😊如果觉得文章不错或能帮助到你学习,可以点赞👍收藏&#x…...

IO流之 File 类和字节流
文章目录一、File 类1. 概述2. 创建功能3. 删除功能4. 判断和获取功能5. 递归策略5.1 递归求阶乘5.2 遍历目录二、字节流1. IO 流概述2. 字节流写数据2.1 三种方式2.2 换行及追加2.3 加异常处理3. 字节流读数据3.1 一次读一个字节3.2 一次读一个字节数组3.3 复制文本文件3.4 复…...

【华为机试真题 Python实现】2023年1、2月高频机试题
文章目录2023年1季度最新机试题机考注意事项1. 建议提前刷题2. 关于考试设备3. 关于语言环境3.1. 编译器信息3.2. ACM 模式使用sys使用input(推荐)3. 关于题目分值及得分计算方式4. 关于做题流程5. 关于作弊2023年1季度最新机试题 两个专栏现在有200博文…...

【拳打蓝桥杯】最基础的数组你真的掌握了吗?
文章目录一:数组理论基础二:数组这种数据结构的优点和缺点是什么?三:数组是如何实现随机访问的呢?四:低效的“插入”和“删除”原因在哪里?五:实战解题1. 移除元素暴力解法双指针法2…...

断崖式难度的春招,可以get这些点
前言 大家好,我是bigsai,好久不见,甚是想念。 开学就等评审结果,还好擦边过了,上周答辩完整理材料,还好都过了(终于可以顺利毕业了),然后后面就是一直安享学生时代的晚年。 最近金三银四黄金…...

一年经验年初被裁面试1月有余无果,还遭前阿里面试官狂问八股,人麻了
最近接到一粉丝投稿:年初被裁员,在家躺平了6个月,然后想着学习下再去面试,现在面试了1个月有余,无果,天天打游戏到半夜,根本无法静下心来学习。下面是他这些天面试经常会被问到的一些问题&#…...

我从功能测试到python接口自动化测试涨到22k,谁知道我经历了什么......
目录:导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜)前言 常见的接口…...

SDG,ADAM,LookAhead,Lion等优化器的对比介绍
本文将介绍了最先进的深度学习优化方法,帮助神经网络训练得更快,表现得更好。有很多个不同形式的优化器,这里我们只找最基础、最常用、最有效和最新的来介绍。 优化器 首先,让我们定义优化。当我们训练我们的模型以使其表现更好…...

UE5 学习系列(二)用户操作界面及介绍
这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…...
应用升级/灾备测试时使用guarantee 闪回点迅速回退
1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间, 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点,不需要开启数据库闪回。…...

从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...
质量体系的重要
质量体系是为确保产品、服务或过程质量满足规定要求,由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面: 🏛️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限,形成层级清晰的管理网络…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...

2021-03-15 iview一些问题
1.iview 在使用tree组件时,发现没有set类的方法,只有get,那么要改变tree值,只能遍历treeData,递归修改treeData的checked,发现无法更改,原因在于check模式下,子元素的勾选状态跟父节…...
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…...
Rapidio门铃消息FIFO溢出机制
关于RapidIO门铃消息FIFO的溢出机制及其与中断抖动的关系,以下是深入解析: 门铃FIFO溢出的本质 在RapidIO系统中,门铃消息FIFO是硬件控制器内部的缓冲区,用于临时存储接收到的门铃消息(Doorbell Message)。…...

云原生玩法三问:构建自定义开发环境
云原生玩法三问:构建自定义开发环境 引言 临时运维一个古董项目,无文档,无环境,无交接人,俗称三无。 运行设备的环境老,本地环境版本高,ssh不过去。正好最近对 腾讯出品的云原生 cnb 感兴趣&…...

HDFS分布式存储 zookeeper
hadoop介绍 狭义上hadoop是指apache的一款开源软件 用java语言实现开源框架,允许使用简单的变成模型跨计算机对大型集群进行分布式处理(1.海量的数据存储 2.海量数据的计算)Hadoop核心组件 hdfs(分布式文件存储系统)&a…...