定时器回调总先于io轮询回调执行,但非绝对抢占,而是由事件循环阶段顺序与当前轮询状态共同决定;timers阶段优先检查并同步执行已到期的settimeout/setinterval;poll阶段在队列为空时会主动让位给timers;微任务始终插在阶段之间,nexttick优先级高于promise;setimmediate在check阶段执行,天然晚于同轮timers但早于下轮timers。

定时器回调在事件循环中总是先于IO轮询阶段的回调执行,但这个“先”不是绝对时间上的抢占,而是由阶段顺序和当前轮询状态共同决定的微观调度结果。
定时器阶段(timers)是事件循环的第一站
每个事件循环周期开始时,Node.js会首先检查 timers 阶段:遍历内部最小堆(或红黑树),找出所有已到期的 setTimeout 和 setInterval 回调并同步执行。这个阶段不等待、不阻塞,只处理“已经明确到期”的任务。
- 即使你设了
setTimeout(fn, 0),它也不会立刻执行,而是被放入 timers 队列,等待下一轮循环的 timers 阶段被轮到 - 定时器的实际触发时间可能延迟——如果上一个阶段(比如 poll)因 I/O 阻塞太久,timers 阶段就会被推迟,导致“不准时”
- 系统级调度、CPU 负载、甚至 Node.js 版本差异,都可能让同一段代码在不同环境下触发顺序出现微小浮动
IO轮询阶段(poll)不只处理IO,还“看时机”决定是否跳回定时器
poll 阶段的核心职责是处理 I/O 完成回调(如 fs.readFile、net.connect),但它还有一个隐藏行为:当 poll 队列为空时,它会主动检查是否有到期定时器,并可能直接跳回 timers 阶段。
详细的 Three.js 3D 图形参考,涵盖场景设置、相机、几何体、材质、光照、动画、控制器、加载器、数学工具和调试。
- 若 poll 队列非空,就挨个执行回调,直到清空或达到上限;此时即使有到期定时器,也得等这一轮 poll 结束
- 若 poll 队列为空,且没有
setImmediate待执行,则 poll 会等待新 I/O 事件——但如果等待期间有定时器到期,它会立刻结束等待,交出控制权给 timers 阶段 - 这个“主动让位”机制,使得在低负载、空闲 I/O 场景下,
setTimeout(0)的响应可以非常接近 0ms
微任务(microtask)始终插在阶段之间
无论 timers 还是 poll 阶段执行完毕,只要同步代码一结束,Node.js 就会立即清空 microtask 队列:包括 process.nextTick() 和所有 Promise.then() 回调。
- 这意味着:哪怕一个
setTimeout(0)已进入 timers 队列,只要它前面有Promise.resolve().then(...),后者一定先执行 -
nextTick的优先级高于 Promise,会在本轮同步代码结束后、任何阶段切换前执行 - 这种插入机制让微任务成为“阶段内不可打断、阶段间必抢占”的关键调节器
setImmediate 是 poll 阶段的“出口守门员”
setImmediate 不属于 timers 阶段,也不在 poll 中执行,而是在 poll 结束后、check 阶段专门处理——这使它天然晚于同轮的 timers,但早于下一轮 timers。
- 在 I/O 回调里调用
setImmediate,它的回调几乎必然在该次 poll 后立即执行,比setTimeout(0)更快(尤其当 timers 阶段被延迟时) - 但在主模块顶层调用
setTimeout(0)和setImmediate,结果可能颠倒——因为首次进入事件循环时,timers 阶段可能还没来得及初始化,poll 先空转一次再进 check - 这不是 bug,而是事件循环启动流程的自然体现:初始 poll 空转 → check → timers → …










