event loop 严格按六个阶段轮转,timer 阶段执行已过期的 settimeout/setinterval 回调,check 阶段执行 setimmediate 回调,二者顺序由阶段位置决定,且各阶段后均清空微任务队列。

Node.js 中的 Event Loop 并不是简单按“谁先注册谁先执行”来排代码顺序,而是严格按六个阶段的轮转节奏推进。Timer 阶段和 Check 阶段的执行次序,取决于它们在事件循环中所处的位置、任务是否已就绪,以及当前 poll 阶段是否阻塞或空闲。
Timer 阶段只处理真正过期的定时器
Timer 阶段对应 setTimeout 和 setInterval 的回调,但它不会在设定时间一到就立刻执行——而是等到事件循环进入该阶段时,才检查哪些定时器已真正过期(即 当前时间 - idleStart ≥ idleTimeout)。即使你写了 setTimeout(fn, 0),它也得等下一轮循环到达 Timer 阶段才能被取出执行。
- 如果多个定时器同时过期,按注册顺序先进先出执行
- 如果 Timer 队列为空,事件循环直接跳过该阶段
- 即使设了 0ms,实际执行仍可能延迟几毫秒,受系统调度影响
Check 阶段紧接在 Poll 阶段之后
setImmediate() 的回调统一放在 Check 阶段执行,而这个阶段总是在 Poll 阶段完成之后才触发。关键点在于:Poll 阶段的行为会直接影响 Check 是否能“马上”运行。
- Poll 队列不为空 → 先清空所有 I/O 回调,再进 Check 阶段
- Poll 队列为空,且没待处理的 I/O → 若有 setImmediate,立即进 Check;否则可能等待新事件或直接跳到下一循环
- 如果 poll 阶段因 I/O 操作阻塞(比如读大文件),Check 就得等它结束才能开始
Timer 和 Check 的相对优先级由循环位置决定
事件循环固定顺序是:Timer → Pending callbacks → Idle/Prepare → Poll → Check → Close callbacks。这意味着:
- 同一轮循环中,Timer 总比 Check 先执行(哪怕 setTimeout(0) 和 setImmediate() 在同一行代码里)
- 但跨轮循环时,情况可能反转:比如 setTimeout(0) 在本轮未触发(因未到 Timer 阶段),而 setImmediate() 已在本轮 Check 阶段执行了
- 常见陷阱:在 I/O 回调(如 fs.readFile)里同时调用 setTimeout(0) 和 setImmediate(),往往后者先输出——因为此时刚退出 Poll,立刻进 Check;而 setTimeout(0) 得等下一轮 Timer 阶段
微任务会插在阶段切换之间
虽然 Timer 和 Check 都是宏任务,但它们各自执行完后,都会立刻清空当前微任务队列(包括 Promise.then、process.nextTick)。所以:
- Timer 阶段执行完一个 setTimeout 回调 → 立即执行所有已排队的 microtask
- Check 阶段执行完一个 setImmediate 回调 → 同样立刻处理 microtask
- 这会导致看似“同步”的嵌套行为,比如在 setTimeout 回调里 new Promise(...),其 then 一定在该 setTimeout 之后、但早于下一个宏任务
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











