Node15 Node中的事件循环
Node中的事件循环,与浏览器中的事件循环,还是有一些不同的。
事件循环
Node.js是单线程的语言,是通过事件循环处理非阻塞I/O操作的,Node会将这些操作转移到系统内核中,内核会在后台处理多种操作。当其中一个操作完成的时候,内核将通知Node将对应的回调函数加入轮询队列中。
Node的I/O处理使用了自己设计的基于事件驱动的跨平台抽象层libuv,它封装了不同操作系统的一些底层特性,对外提供统一的API,事件循环也是有libuv负责
Node中的每次事件循环都包含了6个阶段:
(1)timers阶段:这个阶段执行Timer(setTimeout
、setInterval
)的回调函数
(2)I/O回调阶段:执行一些系统调用错误的回调(比如网络通信的错误回调函数)
(3)idle,prepare阶段:仅供Node内部使用
(4)poll(轮询)阶段:获取新的I/O事件,执行I/O相关的回调函数,适当的条件下将Node阻塞在这里
(5)check阶段:执行setImmediate()
的回调函数
(6)close callbacks阶段:执行一些准备关闭的回调函数,比如执行Socket的close
事件回调
重点关注timers
、poll
和check
三个阶段,日常开发中的绝大部分异步任务都是在这三个阶段处理的。
timers
阶段
在这个阶段,Node会检查有无超时的Timer,如果有则把其回调函数压入timer的任务队列中等待执行。
同浏览器环境一样,Node并不能保证Timer在预设时间到了就会立即执行,因为Node对timer的过期检查不一定靠谱,它会受到系统的影响,比如下面的代码setTimeout
和setImmediate
的执行顺序是不确定的:
1 | setTimeout(() => { |
但是如果在一个I/O回调中,那一定是setImmediate
先执行,因为poll
阶段后面就是check
阶段。
poll
阶段
这个阶段主要有两个功能:
- 处理poll队列的事件
- 如果有超时的timer,则执行timer的回调函数
在这个阶段,Event Loop会同步执行poll队列中的回调函数,直到队列为空,然后Event Loop会去检查check
队列中有无预设的setImmediate()
:
- 有预设的
setImmediate()
,Event Loop将结束poll
阶段进入check
阶段,并执行check
阶段的任务队列 - 如果没有预设的
setImmediate()
,Event Loop检查timer队列是否为空,如果timer非空,则Event Loop开始下一轮事件循环 - 如果timer队列也为空,那么Event Loop将阻塞在该阶段等待。
check
阶段
setImmediate()
的回调函数会被加入check
队列中
process.nextTick()
从语义角度来看,setImmediate
应该与process.nextTick()
名字调换。process.nextTick()
会在各个阶段之间进行,准确的说,是在当前阶段的尾部执行。一旦执行就要直到nextTick队列被清空,才会进入到下一个事件阶段。
nextTick
会在异步任务之前执行。
如果递归调用,会导致Event Loop卡死。
与浏览器事件循环的差异
浏览器环境下,微任务Microtask的任务队列是在每个宏任务Macrotask任务执行完成后执行:
在Node中,Microtask会在事件循环的各个阶段之间执行,也就是在一个阶段执行完毕,就回去执行Microtask队列的任务。
总结
Node.js的事件循环分为了六个阶段,其中常用的是timers
、poll
和check
阶段
Event Loop在每个阶段都有一个任务队列,当执行到某个阶段时将执行该阶段的任务队列,知道队列清空才会进入下一个阶段。
当所有阶段被顺序执行一次后,事件循环就完成了一个Tick。
浏览器环境和Node环境下,Microtask任务队列的执行时机不同:浏览器的Microtask在事件循环的Macrotask执行完成后执行,Node中Microtask会在各个循环阶段之间执行。
练习1
下面的代码在浏览器和Node环境下执行的结果各是什么:
1 | setTimeout(() => { |
浏览器环境下:
1 | timer1 |
Node环境下:
1 | timer1 |
浏览器环境下比较好理解了,每次执行完一次宏任务,都要去检查并执行微任务队列。
在Node环境下,在timer
阶段,先执行timer1
后将promsie1
放到微任务队列,由于Node中的微任务队列是在各个阶段之间执行的,所以此时不会执行微任务队列,而是继续执行第二个timer2
,所以两个setTimeout
先后执行,执行完成后在会执行为微任务。
练习2
1 | process.nextTick(function A() { |
结果是:
1 | 1 |
这是因为process.nextTick
会在每个阶段之间进行,也可以理解为在所有阶段之前进行,它会在所有异步任务之前进行,而且其队列清空之前会持续执行。
练习3
1 | setImmediate(function A() { |
上面代码中,1
和TIMEOUT FIRED
哪个先执行是不确定的,运行结果可能是1--TIMEOUT FIRED--2
,也可能是TIMEOUT FIRED--1--2
。
但是如果放在了一个I/O回调中,执行顺序就是确定的:
1 | const fs = require('fs'); |
在一个I/O回调中,那一定是setImmediate
先执行,因为poll
阶段后面就是check
阶段。