NodeJS事件循环:图文+代码

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实际上是马上执行🤔🤔🤔

后续

需要说明的是,本文中所有代码运行环境的node 10.15.3node 11之后,其事件循环的原理渐渐向浏览器中的事件循环靠拢,比如重要阶段的划分、微任务的执行等。因此本文目前只是一个引子,后续打算分三个阶段再对文章作持续补充:

  1. node 11 前后事件循环原理的变化以及最新的标准,比如pharse的划分和process.nextTick的执行顺序
  2. 浏览器中的JavaSsript引擎线程和事件循环原理
  3. 事件循环案例实践

参考资料:

The Node.js Event Loop, Timers, and process.nextTick() | Node.js (nodejs.org)

Node.js event loop workflow & lifecycle in low level (voidcanvas.com)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值