Node.js 事件循环阶段
解释现代 Node.js/libuv 的 phases、poll/check/timers 职责,以及 process.nextTick 与 V8 microtasks 的配合方式。
[!info] related notes
Node.js 事件循环阶段
一句话定义
Node.js Event Loop 由 libuv 驱动,不应只用“宏任务 / 微任务”理解;它由 I/O phases 配合 process.nextTick 和 V8 microtasks 调度 JavaScript。
Node 默认只有一条 JavaScript 执行主线,但整个 Runtime 不是单线程:大量网络 I/O 由操作系统处理,部分文件、DNS、crypto 等工作还会使用 libuv Worker Pool。
核心 phases
现代 Node.js 可以重点理解:
pending callbacks
→ idle / prepare
→ poll
→ check
→ close callbacks
→ timers
→ 下一轮
Node.js 20 / libuv 1.45 起,正常 event-loop iteration 中 timers 在 poll 之后运行;早期资料常画成 timers 在 poll 之前。
pending callbacks
处理部分延迟到下一轮的系统级 I/O callbacks,例如某些 TCP error callbacks。
idle / prepare
libuv / Node 内部使用,业务代码通常不直接关心。
poll
最关键的 phase 之一:计算等待 I/O 的时长、获取新 I/O events、执行 poll queue 中的大部分 callbacks。
check
执行 setImmediate(callback)。check 不负责 close callback。
close callbacks
执行部分资源关闭回调,例如 socket.on('close', callback)。
timers
执行达到时间阈值的 setTimeout / setInterval callbacks。delay 是最小阈值,不保证到点立刻执行。
nextTick 与 V8 microtasks
当前 JavaScript callback / operation 完成
→ process.nextTick queue
→ V8 microtasks
├─ Promise.then / catch / finally
└─ queueMicrotask
→ 继续 Event Loop phase
因此通常:
Promise.resolve().then(() => console.log('promise'))
process.nextTick(() => console.log('nextTick'))
输出:
nextTick
promise
递归注册 nextTick 会造成 starvation,使 timer 和 I/O 长时间无法运行。
setImmediate 与 setTimeout(0)
顶层代码里不要背死顺序,实际取决于运行位置和 timer 是否达到阈值。在 I/O callback 中,当前从 poll 处理完 I/O 后通常进入 check,所以 setImmediate 更容易先于新建的 timer 执行。
学习检查
不看正文回答:
- Node.js Event Loop 为什么不能只用“宏任务 / 微任务”两类解释?
- poll、check、timers、close callbacks 分别负责什么?
process.nextTick为什么不属于 libuv phase?- nextTick 与 V8 microtasks 在当前 JavaScript callback 返回后如何排序?
- 为什么
setImmediate与setTimeout(0)在顶层代码里不能死背固定顺序? - 递归 nextTick 为什么可能让 I/O 或 timer starvation?
- 给一段在 I/O callback 内同时安排 nextTick、Promise、setImmediate、setTimeout 的代码,先写出调度层级再预测输出。