Node.js的架构
作为一个服务端框架,Node.js在运行时有许多依赖,其中最重要的两个是V8引擎和libuv库
V8引擎使得Node.js能够运行JavaScript代码,是用JavaScript和C++开发的*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 Server、WebSocket时触发的回调函数
以及两个特殊的回调队列:
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.nextTick和setImmediate的名字应该互换一下,这样才与它们实际的执行机制相符,因为setImmediate实际上在下一个循环执行,nextTick实际上是马上执行🤔🤔🤔
最后
为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。



有需要的小伙伴,可以点击下方卡片领取,无偿分享
本文详细介绍了Node.js的事件循环机制,包括事件循环的各个阶段、线程池的工作原理,以及如何通过代码实例理解事件循环的执行顺序。文章通过图文并茂的方式,帮助读者深入理解Node.js中异步编程的核心——事件循环。
1436

被折叠的 条评论
为什么被折叠?



