Node事件循环

130 阅读2分钟

事件循环图

event-loop-phase.png

event-loop.png

event-loop-explain.png

事件循环圈

程序每走上图2中的一个圈叫做一次循环,一次循环要经历六个阶段:

  1. timers:这个阶段执行被setTimeout和setInterval注册的回调.
  2. pending callback: 执行被推迟到下一个轮询迭代的I\O回调.
  3. idle prepare:仅供内部使用.
  4. poll:检索新的I\O事件;执行与I\O相关的回调(除了timers、close callback、setImmediate);node 将在适宜的时候在次阻塞.
  5. check:setImmediate回调将在次阶段调用.
  6. 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饥饿。

参考鸣谢:juejin.cn/post/701030…