NodeJS 中的事件循环,读了这篇就全懂了
事件循环是 NodeJS 处理非阻塞 I/O 操作的和核心机制。NodeJS 的事件循环脱胎于 libuv 的事件循环,因此,要搞清楚 NodeJS 的事件循环,还需要先了解 libuv 的事件循环是如何工作的。
libuv 的事件循环
我们先来了解两个基本概念:句柄(handle)和请求(request).
- 句柄是指在整个事件循环活跃时间内能够执行某些操作的长期对象。比如一个 TCP 服务句柄,每当有新的联接建立时,这个句柄的
connected
回调就会被调用。 - 请求是通常指短期操作。比如向某个句柄中写入数据的操作。
了解了这两个概念以后,我们来看看 libuv 的事件循环是如何工作的。
下面这张图可以清楚的展示事件循环的执行过程:
结合这张图我们简单描述一下一次循环过程中各个步骤做了什么。
- 首先更新循环内的当前时间(now),避免在循环过程中多次发生与时间相关的系统调用。
- 检查当前事件循环是否还是活跃(active)的。检查的表示是当前事件循环是否还有活跃的句柄、活跃的请求操作,或者还有“关闭”回调的话,就视为是活跃的。如果判断当前循环不是活跃的,则直接退出。
- 执行所有的到期回调。即所有的到期时间在循环当前时间之前的回调都会被执行。
- 执行所有的挂起回调(pending callbacks)。所谓挂起回调,就是在上一个循环周期中设置的到下一循环周期在执行的回调。
- 执行空闲句柄回调(idle handle callbacks)。虽然名字中包含空闲二字,实际上每个循环周期都会执行。
- 执行准备句柄回调(prepare handle callbacks)。
- 在这一步会暂停循环,轮询等待 I/O 事件一段时间。这个时间长度是根据一个算法算出,这里不做详细说明。在轮询期间,所有 I/O 相关的回调会被执行(前提是系统通知到 libuv)。
- 执行检查句柄回调(check handle callbacks)。检查句柄回调往往与准备句柄回调相对应。这两个回调可以方便我们在 I/O 之前做一些准备工作,然后在 I/O 之后做相应的检查。
- 执行关闭回调(close callbacks)。比如通过
uv_close()
设置的回调。
整个事件循环就是 1 - 9 的循环执行。
值得说明的是,libuv 会在轮询阶段中断事件循环,等待系统通知。比如某个文件 I/O 已经完成,或者接收到一个网络连接等。在接收到系统通知后,事件循环会调用相关的回调执行操作。
不同的平台(windows\linux 等),异步 I/O 的机制不同,libuv 底层会根据不同平台,采用不同的 I/O 轮询机制,比如 epoll(linux)、kqueue(OSX)、IOCP(windows)等,上层不需要关注异步 I/O 的实现机制。
NodeJS 的事件循环
现在我们来看 NodeJS 的事件循环。同样,我们放一张 NodeJS 事件循环的过程图。
在 NodeJS 中,事件循环的每一步成为一个阶段,每个阶段都有一个 FIFO 队列来执行回调。通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列清空或达到最大回调数限制。当队列清空或者达到最大限制,事件循环进入下一阶段。
对比两个事件循环的图,我们可以看到,具体过程基本相同。因此,NodeJS 的事件循环过程我们简述如下:
- 定时器阶段,执行已经被
setTimeout()
和setInterval()
调度的回调函数。 - 挂起的回调,执行(在上一个循环中被设置)延迟到下一个循环迭代的 I/O 回调。
- idle, prepare 阶段,仅 NodeJS 系统内部使用。
- 轮询阶段,检索新的 I/O 事件,执行与 I/O 相关的回调。与 libuv 一样,NodeJS 还在这个阶段暂停循环一段时间。
- 检测阶段,执行被
setImmediate()
调度的回调函数。 - 关闭的回调函数,执行一些关闭的回调函数,如:
socket.on('close', ...)
。
我们对轮询阶段做个详细说明。
轮询阶段有两个重要的功能:
- 计算应该阻塞和轮询 I/O 的时间。
- 处理轮询队列里的事件。
一旦事件循环进入轮询阶段并且没有到期的定时器回调时,事件循环将做如下判断:
- 如果轮询队列不是空的,那么事件循环将循环访问回调队列并同步执行它们,直到清空队列,或者达到了最大限制。
- 如果轮询队列是空的,则再做如下判断:
- 如果有代码是被
setImmediate()
调度的,那么事件循环将结束轮询阶段,并到检查阶段以执行那些被调度的代码。 - 如果没有代码被
setImmediate()
调度,那么事件循环将等待回调被添加到队列中,然后立即执行。
- 如果有代码是被
在轮询阶段的执行过程中,一旦轮询队列为空,事件循环将检查是否有到期的定制器。如果一个或多个定时器已准备就绪,则事件循环将绕回定时器阶段以执行这些定时器的回调。
这里要特别对 setImmediate()
进行一些说明。
在 libuv 的事件循环中,允许开发人员在轮询阶段之前做些准备操作,然后在轮询阶段之后立即对这些操作进行检查。NodeJS 中 setImmediate()
实际上是一个在事件循环的单独阶段运行的特殊定时器。它使用一个 libuv API 来安排回调在轮询阶段完成后执行。
setImmediate
、setTimeout
和 process.nextTick
setImmediate()
被设计为一旦在当前轮询阶段完成,就执行代码。setTimeout()
是在最小阈值(ms 单位)过后执行代码。process.nextTick()
严格意义上讲并不属于事件循环的一部分。它不管事件循环的当前阶段如何,它都将在当前操作完成后处理nextTickQueue
中排队的代码。
setImmediate()
和 setTimeout()
很类似,但是基于被调用的时机,他们也有不同表现。
我们看下面这段代码:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
这两个函数调用都在主模块中被调用,则他们的回调执行顺序是不定的,受进程的性能影响很大(进程会受到系统中运行其他应用程序影响)。
但是一旦将这两个函数放到 I/O 轮询调用内,那么 setImmediate()
一定会在 setTimeout()
之前被执行,不管有多个定制器已经到期。比如下面这段代码,总是会先输出 "immediate"。
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
process.nextTick()
和 setImmediate()
严格意义上来说,应该将名称互换。因为 process.nextTick()
比 setImmediate()
触发得更快。
任何时候在给定的阶段中调用 process.nextTick()
,所有传递到 process.nextTick()
的回调将在事件循环继续之前解析。之所以这么设计,是考虑到这些使用场景:
- 允许开发者处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。
- 有时有让回调在栈展开后,但在事件循环继续之前运行的必要。
比如下面这段代码:
const server = net.createServer(() => {}).listen(8080);
server.on('listening', () => {});
只有传递端口时,端口才会立即被绑定,然后立即调用 'listening'
回调。问题是 .on('listening')
的回调在那个时间点尚未被设置。
为了绕过这个问题,'listening'
事件被排在 nextTick()
中,以允许脚本运行完成。这让用户设置所想设置的任何事件处理器。
Promise
这里在补充说明一下 NodeJS 中 Promise 是如何处理的。我们之前说过,在浏览器的事件循环里,会有一个微任务的队列来防止所有的微任务,并且在每个操作之后,都尝试清空微任务队列。
在 NodeJS 中,做法类似,NodeJS 的事件循环中也有一个微任务队列,工作机制与 process.nextTick()
类似,在每个操作之后,事件循环都会尝试清空微任务队列。
总结
我们结合 libuv 的事件循环,详细说明了 NodeJS 事件循环的每一阶段的具体职能。同时,我们还分析了常用的几个异步代码函数的原理。
我们用一张图归纳如下:
常见面试知识点、技术解决方案、教程,都可以扫码关注公众号“众里千寻”获取,或者来这里 https://everfind.github.io 。
NodeJS 中的事件循环,读了这篇就全懂了的更多相关文章
- 深入理解javascript中的事件循环event-loop
前面的话 本文将详细介绍javascript中的事件循环event-loop 线程 javascript是单线程的语言,也就是说,同一个时间只能做一件事.而这个单线程的特性,与它的用途有关,作为浏览器 ...
- nodeJS中的事件机制
events模块是node的核心模块,几乎所有常用的node模块都继承了events模块,比如http.fs等.本文将详细介绍nodeJS中的事件机制 EventEmitter 多数 Node.js ...
- JavaScript中的事件循环机制跟函数柯里化
一.事件循环机制的理解 test();//按秒输出5个5 function test() { for (var i = 0; i < 5; i++) { setTimeout(() => ...
- 【nodejs原理&源码赏析(7)】【译】Node.js中的事件循环,定时器和process.nextTick
[摘要] 官网博文翻译,nodejs中的定时器 示例代码托管在:http://www.github.com/dashnowords/blogs 原文地址:https://nodejs.org/en/d ...
- 【nodejs原理&源码赏析(7)】【译】Node.js中的事件循环,定时器和process.nextTick
目录 Event Loop 是什么? Event Loop 基本解释 事件循环阶段概览 事件循环细节 timers pending callbacks poll阶段 check close callb ...
- TMsgThread, TCommThread -- 在delphi线程中实现消息循环(105篇博客,好多研究消息的文章)
在delphi线程中实现消息循环 在delphi线程中实现消息循环 Delphi的TThread类使用很方便,但是有时候我们需要在线程类中使用消息循环,delphi没有提供. 花了两天的事件研究了 ...
- nodejs笔记之事件循环
Event Loop (事件循环或者事件轮询) Event Loop是一个程序结构,用于等待和发送消息和事件. 简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为"主线程&q ...
- JavaScipt 中的事件循环(event loop),以及微任务 和宏任务的概念
说事件循环(event loop)之前先要搞清楚几个问题. 1. js为什么是单线程的? 试想一下,如果js不是单线程的,同时有两个方法作用dom,一个删除,一个修改,那么这时候浏览器该听谁的? ...
- JavaScript中的事件循环
JavaScript是单线程单并发语言 单线程:主程序只有一个线程,即同一时间片段内其只能执行单个任务. 引发的问题: 单线程,意味着任务都需要排队,前一个任务结束,才会执行后一个任务.若前一个任务耗 ...
随机推荐
- VSCode 使用 Code Runner 插件无法编译运行文件名带空格的文件
本文同时在我的博客发布:VSCode 使用 Code Runner 插件无法编译运行文件名带空格的文件 - Skykguj 's Blog (sky390.cn) 使用 Visual Studio C ...
- Libevent2.1.8版在Liunx中编译安装遇到的问题
Libevent2.1.8版在Liunx中编译安装遇到的问题 前言:在网上找了很久,都没有一个明确的解决方法,通过分析可能的原因,将自己实际操作及解决的成功结果记录如下,以供遇到相似的问题,能提供思路 ...
- JavaWeb入门知识梳理
万维网 Web App(Web应用程序)是一种可以通过万维网访问的应用程序,用户只需要连接互联网和计算机安装浏览器,即可通过URI在线使用某个Web App,而不需要再安装客户端到计算机上.Web A ...
- AcWing 1289. 序列的第k个数
BSNY 在学等差数列和等比数列,当已知前三项时,就可以知道是等差数列还是等比数列. 现在给你 整数 序列的前三项,这个序列要么是等差序列,要么是等比序列,你能求出第k项的值吗. 如果第k项的值太大, ...
- redis集群搭建中遇到的一些问题
redis单机模式启动后,修改完配置文件,使用以下命令创建redis集群: sudo ./src/redis-trib.rb create --replicas 1 ip1:6379 ip2:6379 ...
- Linux:CentOS-7配置VMware-15.5与本机IP同网段
确认本机IP ctrl+R:输入cmd 回车 输入命令:ipconfig 可以看出一下信息:本机ip: 192.168.1.162网关:192.168.1.1DNS服务器:192.168.1.1 设 ...
- shell运维习题训练
注:初学shell,以下为本人自己写的答案,如果有更好的,请指教! 1. 求2个数之和: 2. 计算1-100的和 3. 将一目录下所有的文件的扩展名改为bak 4.编译并执行当前目录下的所有.c文件 ...
- Leetcode No.108 Convert Sorted Array to Binary Search Tree(c++实现)
1. 题目 1.1 英文题目 Given an integer array nums where the elements are sorted in ascending order, convert ...
- SA20225394 舒蔚 高级软件工程实验总结
[实验]: 一.编程神器Visual Studio Code 配置好Visual Studio Code这一强大而又轻量的编辑器.其中有版本控制+代码补全+自动加载依赖,并且可以根据自己的需要添加插件 ...
- kali中设置共享文件夹
1.在虚拟机设置共享目录 2.查看共享目录命令 root@kali:~# vmware-hgfsclient 3.新建文件夹 root@kali:~# mkdir /mnt/hgfs/ShareDir ...