定时器回调优先于I/O轮询回调执行,但poll阶段会主动让位给timers;timers只处理已到期任务,poll空闲时检查并交还控制权;微任务总在阶段切换时立即执行;setImmediate在check阶段运行,介于poll与下轮timers之间。

定时器回调和 I/O 轮询回调不是“谁抢到谁先跑”的竞争关系,而是由事件循环阶段顺序与当前队列状态共同决定的协作流程。核心在于:timers 阶段永远在 poll 阶段之前,但 poll 阶段会主动让位——这构成了二者实际交互的关键逻辑。
timers 阶段只处理“已明确到期”的任务
每个事件循环周期开始时,Node.js 进入 timers 阶段,检查内部定时器堆,只同步执行那些时间阈值已到的 setTimeout/setInterval 回调。它不等待、不预测、不提前触发——哪怕你写了 setTimeout(fn, 0),它也只是被标记为“可执行”,要等到下一轮循环的 timers 阶段才真正运行。如果上一阶段(比如 poll)因 I/O 积压耗时过长,这个“0ms”就可能延迟几十毫秒甚至更久。
poll 阶段会动态判断是否交出控制权
poll 阶段主要处理已完成的 I/O 回调(如 fs.readFile 结束后的回调),但它有两个关键行为:
详细的 Three.js 3D 图形参考,涵盖场景设置、相机、几何体、材质、光照、动画、控制器、加载器、数学工具和调试。
- 当 poll 队列非空:挨个执行回调,直到清空或达到上限;此时即使有新到期的定时器,也得等这一轮 poll 完结
- 当 poll 队列为空:它不会立刻跳到下一阶段,而是先检查是否有到期定时器;如果有,就立即结束等待,把控制权交还给 timers 阶段
这个机制让低负载场景下的定时器响应非常及时,也解释了为什么空闲时 setTimeout(fn, 0) 看起来像“立刻执行”。
微任务始终插在阶段切换的缝隙里
无论 timers 还是 poll 阶段执行完同步代码,Node.js 都会立刻清空 microtask 队列——process.nextTick() 和所有 Promise.then() 回调都在此时运行。这意味着:
- 一个刚加入 timers 队列的
setTimeout(fn, 0),若前面有Promise.resolve().then(...),后者一定先执行 -
nextTick的优先级高于 timers 和 poll 回调,但滥用会导致 I/O 回调长期得不到执行(I/O 饥饿)
setImmediate 是“本轮最后、下轮最先”的桥梁
setImmediate() 的回调在 check 阶段执行,位置在 poll 之后、下一轮 timers 之前。因此:
- 它天然晚于同轮的 timers 回调(哪怕 timers 是
setTimeout(fn, 0)) - 但早于下一轮 timers,所以常被用来“让出本轮、确保 I/O 已完成后再执行”
- 如果 poll 队列为空且没有 setImmediate 待执行,事件循环可能直接进入 timers;但一旦有 setImmediate,它就会保证在 poll 后、timers 前执行

















