来了解NodeJS的事件循环原理吧:图文+代码

本文详细介绍了Node.js的事件循环机制,包括事件循环的各个阶段、线程池的工作原理,以及如何通过代码实例理解事件循环的执行顺序。文章通过图文并茂的方式,帮助读者深入理解Node.js中异步编程的核心——事件循环。

Node.js的架构

作为一个服务端框架,Node.js在运行时有许多依赖,其中最重要的两个是V8引擎和libuv

  • V8引擎使得Node.js能够运行JavaScript代码,是用JavaScriptC++开发的* libuv使得Node.js能够进行文件操作、网络操作等,是用C++开发的。其内部实现了事件循环和线程池:* 事件循环负责处理简单的任务,比如执行回调函数、网络IO等* 线程池负责处理更加复杂的任务,比如文件访问、压缩等
  • 其它依赖* http-parser:用于解析http请求和响应* c-ares:异步DNS解析库,可以和事件循环统一起来,实现DNS的非阻塞异步解析* crypto(OpenSSL):用于实现安全通信,加密解密* zlib:用于压缩和解压缩

Node 进程,线程和线程池

当我们运行Node.js时,计算机后台便会开启一个Node.js的进程(Node.js本身也提供了用于进程管理的API)

与此同时,Node.js的运行是单线程的,也就是说不管有多少用户在访问应用程序,所有指令都在一个线程中执行,这使得它非常容易被堵塞。具体来说,当Node.js被启动时,会在单线程中依次执行以下操作:

初始化项目👉执行顶层代码(不在回调函数中)👉加载模块👉注册回调函数👉开启事件循环(回调函数中)

  • 其中,事件循环扛起了一片天,会执行程序中的大部分任务,但有些任务确实过于复杂,如果在事件循环中执行,就会阻塞整个线程。time for 线程池
  • 线程池提供了4个与主线程完全分开的线程,事件循环在遇到诸如文件、密码、压缩、DNS查询等复杂操作时,就会将这些任务交给线程池

事件循环

如果要用一句话概括事件循环的作用,按我的理解就是事件循环会接收事件、执行所有在回调函数中的代码,并将复杂的任务交付给线程池

事件循环有多个阶段,每个阶段都有自己的一个回调函数队列,其中四个重要的阶段

1.普通计时器阶段:setTimeout()
2.I/O任务阶段:http()fs()等文件和网络处理相关
3.特殊计时器阶段:setImmediate()
4.close阶段:如关闭Web ServerWebSocket时触发的回调函数

以及两个特殊的回调队列

1.process.nextTick()的回调:需要在当前事件循环结束之后立即执行某个特定回调时使用,类似setImmediate(),不同的是setImmediate()是在文件和网络处理之后执行
2.其它微任务的回调(Resolved promises)

上面的序号即表示执行的优先级

重要阶段的回调,以setTimeout()为例,当事件循环到了普通定时器阶段,如果此时计时结束了,会立即执行setTimeout()中的回调函数;如果没有结束,事件循环会继续到下一个阶段,而且在这次circle中不会再执行这个定时器中的回调,就算在这期间计时结束了。直到下次进入定时器阶段再执行。

而那两个特殊的回调队列,以process.nextTick()为例,虽然名称叫nextTick,但process.nextTick()中的回调并不会在下一个tick中执行,而是在当前阶段(parse)结束后立即执行,也就是说,假如事件循环执行到了I/O事件阶段,此时有process.nextTick()中的回调需要执行,那么在执行完当前I/O事件的回调之后便会立即执行process.nextTick()中的回调,之后再执行特殊计时器的回调。

那么Node.js是如何判断事件循环的一个circle是否结束的?在执行完close阶段的回调函数后,Node.js会检查是否有还在计时的定时器或I/O任务,如果还有那就进行事件循环的下一轮circle;如果没有就直接退出事件循环,也就退出Node.js的程序了

为了更加直观,我将上面叙述的Node.js程序启动到退出的整个过程依照个人的理解绘制成了流程图:

代码

初来乍到,先看一段代码热热身

const fs = require("fs");

setTimeout(() => {console.log("Timer 1 finished"), 0;});

setImmediate(() => {console.log("Immediate 1 finished"); });

fs.readFile("test-file.text", () => {console.log("I/O finished"); });

console.log("Hello from the top-level code"); 

以上代码输出结果:

// node 10.15.2
Hello from the top-level code
Timer 1 finished
Immediate 1 finished
I/O finished 

有的人可能运行的结果是

// node 10.15.2
Hello from the top-level code
Timer 1 finished
Immediate 1 finished
I/O finished 

究其原因,需要补充一个知识点:Node.js会把setTimeout(fn, 0)强制改为setTimeout(fn, 1)官方文档有介绍,这是源码决定的。所以关键就在这个1ms秒,如果同步代码执行时间较长,进入普通定时器阶段时1ms已经过了,那么setTimeout执行,否则就先执行了setImmediate。每次我们运行脚本时,机器状态可能不一样,导致运行时有1ms的差距

下面再小试牛刀一下

const fs = require("fs");

setTimeout(() => {console.log("Timer 1 finished"), 0;});

setImmediate(() => {console.log("Immediate 1 finished");});

fs.readFile("test-file.text", () => {console.log("I/O finished"); console.log("-------"); // 方便直观的区分setTimeout(() => {console.log("Timer 2 finished"), 0;});setTimeout(() => { console.log("Timer 3 finished"), 3000;});setImmediate(() => {console.log("Immediate 2 finished");});process.nextTick(() => { console.log("Process.nextTick");});
});

console.log("Hello from the top-level code"); // 同步代码 

与第一段代码相比,在fs.readFile中增加了三个定时器和一个process.nextTick,输入结果如下:

Hello from the top-level code
Timer 1 finished
Immediate 1 finished
I/O finished
-------
Process.nextTick
Immediate 2 finished
Timer 2 finished
Timer 3 finished 

---上方的输出与之前的一样,在进入fs.readFile的回调后遇到process.nextTick会立马执行,之后再继续执行其它内容,在进入定时器阶段时,Timer 3还处于pending的状态,因此只能到下一个循环中执行,至于 setImmediate先于setTimeout执行,是因为在轮询时发现有setImmediate就会执行setImmediate中的回调

总结

  • 事件循环会执行所有在回调函数中的代码,并将复杂的任务交付给线程池
  • 在同一个异步回调中,setImmediate总是比setTimeout(fn, 0)先执行;如果都在顶层代码或者setImmediate回调里,执行的顺序取决于当时机器的状况
  • process.nextTicksetImmediate的名字应该互换一下,这样才与它们实际的执行机制相符,因为setImmediate实际上在下一个循环执行,nextTick实际上是马上执行🤔🤔🤔

最后

为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值