NodeJS的Event Loop是用Libuv实现的。核心驱动为uv_run函数,使用的是UV_RUN_ONCE模式,尽可能在一次uv_run周期中处理I/O的回调。(loop alive会判断有没有进行中的异步请求。)
NodeJS的一次Event Loop周期会经历N个阶段,每个阶段结束后会进入下个阶段。来自官方NodeJS文档。
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
timer:处理就绪的timer 回调队列。(setTimeout或setInterval的callback)
I/O:处理就绪的I/O 回调队列。(对应uv_run的pending阶段)
idle,prepare:处理就绪的internal 回调队列。
poll:处理就绪的I/O 回调队列或阻塞。
check:处理就绪的check 回调队列。(setImmediate的callback)
close:处理就绪的句柄关闭回调。(资源回收时设定的close 回调)
当一个阶段将当前属于自己的回调队列处理完毕就会进入下一个阶段。所谓的“就绪”,是指进入这个【阶段前】就已经完成异步请求准备触发的回调。
poll阶段比较特殊,他有可能阻塞Node的线程。
如果在当前Event Loop周期有过I/O回调事件的产生,又或是其他阶段有异步回调可以处理,那么poll阶段就不会阻塞。
如果没有,Node的线程就会在这阶段休眠,等待I/O或timer回调事件的产生。
poll阶段的阻塞作用在于没有可执行的异步事件时,就此休眠线程,避免忙轮询。(查资料很容易得知,poll阶段内核是选择各个平台最好的实现。然而,似乎为了统一在各个平台的Event Loop,这个阶段只会被I/O回调事件唤醒或超时结束。)
Event Loop在选择低耗能的阻塞线程前,会尽可能地完成一次Loop周期,这样的行为正好说明了它非常注重每种异步回调的实时性。至于为什么要划分如此多的队列和阶段就不得而知了。
实际上,在一次Event Loop的close阶段后,还会进行一次timer回调队列的处理。因为有poll阶段的阻塞耗时,这时很可能有超时的回调可以执行。
值得一提的是,process.nextTick和Promise事件会往microTask队列加入回调,而microTask会在每个阶段结束时清空,这时Event Loop才进入下一个阶段。