事件循环图
事件循环圈
程序每走上图2中的一个圈叫做一次循环,一次循环要经历六个阶段:
- timers:这个阶段执行被setTimeout和setInterval注册的回调.
- pending callback: 执行被推迟到下一个轮询迭代的I\O回调.
- idle prepare:仅供内部使用.
- poll:检索新的I\O事件;执行与I\O相关的回调(除了timers、close callback、setImmediate);node 将在适宜的时候在次阻塞.
- check:setImmediate回调将在次阶段调用.
- close callback:一些close回调,例如 socket.on('close', ...).
其中 2、3、6是系统级别的,我们不关心,重点关注timers、poll、check。
工作原理
- 每一个阶段都会维护一个FIFO队列。
- 每一次循环从timers开始、按上图的顺序依次检查。
- 当到达一个阶段后,检查当前阶段队列中的回调,如果没有,则进入下一阶段。如果有,就依次执行,直到队列被清空或者次数达到上限。
- poll阶段是核心,当进入poll阶段时,首先会检查队列中是否有回调,如果有则同步遍历执行。如果队列为空、也没有调度定时器并且脚本中有调度setImmediate,则会结束poll阶段,进入check阶段执行回调;如果脚本中也没有调度setImmediate,那么事件循环就会在poll阶段进行等待,等待回调被加入进队列中。
- 在每次运行事件循环之间,nodejs 会检查是否有正在等待任何异步 I\O 或定时器,如果没有,就会关闭。
setTimeout VS setImmediate
- setImmediate:在check阶段执行,目的是在当前poll阶段完毕后立即执行
- setTimeout:在timers阶段执行,即使计时到达,但是事件轮询如果没有触达timers阶段,依然不会执行
思考一下setTimeout(() => {}, 0) 和 setImmediate 谁更快?
结论:在I\O周期内,一定是先执行setImmediate,后执行setTimeout;在主线程中顺序是不可控的,受当时进程性能的影响。
process.nextTick()
可以简单理解成微任务,执行优先级最高,在当前阶段使用后回调任务会被添加到微任务队列中,在下一阶段开始前会清空这个队列,否则不会进入下一个阶段。
优点
- 快速响应:
process.nextTick()允许你尽可能快地延迟调用。 - 避免堆栈溢出:递归调用函数时,可以避免v8引擎调用堆栈溢出,因为它每次迭代时都会清空调用堆栈。
缺点
- 饥饿I/O:由于
process.nextTick()在进入下一个事件轮询阶段前,必须清空队列,如果这个队列被无线填充,那么I/O操作会被无限期的推迟,导致I/O饥饿。