事件循环
Javascript是单线程的,但是nodejs通过事件循环的方式实现了非阻塞的IO。
Javascript主线程通过将发起的IO操作封装请求对象传递给IO线程池,主线程继续完成自己接下来的工作。IO线程池中利用空闲的线程完成IO操作后,会进入事件循环阶段阶段,在此阶段取出IO结果和回调函数进行执行
事件循环的阶段

每个阶段都有一个需要执行的回调函数的先入先出(FIFO)队列。同时,每个阶段都是特殊的,基本上,当事件循环进行到某个阶段时,会执行该阶段特有的操作,然后执行该阶段队列中的回调,直到队列空了或者达到了执行次数限制。这时候,事件循环会进入下一个阶段,循环往复。
阶段总览
- 计时器(timers):本阶段执行setTimeout() 和 setInterval() 计划的回调
- I/O 回调: 执行几乎全部发生异常的 close 回调, 由定时器和setImmediate()计划的回调;
- 空闲,预备(idle,prepare):只内部使用;
- 轮询(poll): 获取新的 I/O 事件;nodejs这时会适当进行阻塞;
- 检查(check): 调用 setImmediate() 的回调;
- close callbacks: 例如 socket.on('close', ... );
阶段细节(着重 timer, poll, check)
定时器(timers)
定时器的用途是让指定的回调函数在某个阈值后会被执行,具体的执行时间并不一定是那个精确的阈值。定时器的回调会在制定的时间过后尽快得到执行,然而,操作系统的计划或者其他回调的执行可能会延迟该回调的执行。
注:从技术上来看,轮询阶段控制了定时器的执行时机。
例如,你设定了在100ms后执行某个操作,然后脚本开始执行一个需要95ms的文件读取操作。当文件读取完成时,轮询阶段加入了回调函数,假设回调函数执行10ms完成,这时poll阶段回调队列清空完成了,此时才会去执行timer阶段回调。
轮询(poll)
轮询阶段有两个主要功能:
- 执行已经到时的定时器脚本
- 处理轮询队列中的事件。
当事件循环进入到轮询阶段却没有发现定时器时:
- 如果轮询队列非空,事件循环会迭代回调队列并同步执行回调,直到队列空了或者达到了上限(根据操作系统的不同而设定的上限)
- 如果轮询队列是空的
- 如果有setImmediate()定义了回调,那么事件循环会终止轮询阶段并进入检查阶段去执行定时器回调;
- 如果没有setImmediate(),事件回调会等待回调被加入队列并立即执行。
一旦轮询队列空了,事件循环会查找已经到时的定时器。如果找到了,事件循环就回到定时器阶段去执行回调。
检查(check)
这个阶段允许回调函数在轮询阶段完成后立即执行。如果轮询阶段空闲了,并且有回调已经被 setImmediate() 加入队列,事件循环会进入检查阶段而不是在轮询阶段等待。
setImmediate() vs setTimeout()
这两个很相似,但调用时机会的不同会导致它们不同的表现。
- setImmediate() 被设计成一旦轮询阶段完成就执行回调函数;
- setTimeout() 规划了在某个时间值过后执行回调函数;
这两个执行的顺序会因为它们被调用时的上下文而有所不同。如果都是在主模块调用,那么它们会受到进程性能的影响
但是如果把它们放进 I/O 循环中,setImmediate() 的回调总是先执行,setImmediate() 比 setTimeout() 优势的地方是 setImmediate() 在 I/O 循环中总是先于任何定时器,不管已经定义了多少定时器。