Node.js事件循环由libuv实现,含timers、pending callbacks、idle/prepare、poll、check、close callbacks六阶段,每阶段后检查微任务队列,process.nextTick优先级最高。

JavaScript 本身不处理 Node.js 的六个事件循环阶段——这是 libuv 的职责。Node.js 的事件循环机制和浏览器不同,它的调度逻辑完全由 C 语言编写的 libuv 库实现,V8 引擎只负责执行 JavaScript 代码、管理内存和微任务队列,不参与阶段流转。
真正决定“哪个回调先跑、哪个后跑”的,是 libuv 每轮 tick 中对六个阶段的严格顺序执行,以及 Node.js 在每个阶段结束后插入的微任务检查。
下面直接说清楚关键点:
Node.js 事件循环的六个阶段怎么走
timers 阶段
执行已到期的setTimeout和setInterval回调。注意:设定时间为 0 并不等于立刻执行,它只是被放进 timers 队列,要等这一轮循环走到该阶段才可能执行。pending callbacks 阶段
处理上一轮未完成的系统级 I/O 回调,比如某些 TCP 错误(ECONNREFUSED)会在这一阶段触发。业务代码几乎不会直接碰到。idle / prepare 阶段
完全内部使用,开发者无需关注,也不应依赖。-
poll 阶段
核心阶段。它做两件事:- 如果有就绪的 I/O 回调(如
fs.readFile完成、HTTP 请求响应到达),立即执行; - 如果没有,且没有待处理的 timers,事件循环可能在这里阻塞等待新 I/O 事件。
这个阶段不执行setImmediate,也不执行微任务。
- 如果有就绪的 I/O 回调(如
check 阶段
专门执行setImmediate回调。它总在 poll 阶段之后、close callbacks 之前运行。close callbacks 阶段
执行资源关闭类回调,比如socket.on('close', ...)或server.close(...)的回调。
每轮 tick 结束后,Node.js 主动检查一次微任务队列(包括 Promise.then/catch/finally、queueMicrotask),并清空它——这一步不在 libuv 六阶段内,是 Node.js 自己加的逻辑。
立即学习“Java免费学习笔记(深入)”;
微任务和宏任务的关系很明确
- 所有六个阶段里的回调(
setTimeout、fs.readFile回调、setImmediate等)都属于宏任务,按阶段顺序排队。 - 每个宏任务执行完后,立刻、同步地执行全部当前微任务,不跨阶段、不延迟。
-
process.nextTick是个特例:它比微任务还早,在每个阶段内部回调执行完毕后、进入下一阶段前就清空一次。所以它优先级最高,但滥用会导致 I/O 饥饿。
实际执行顺序看这个典型例子
console.log('1');
setTimeout(() => console.log('2'), 0);
setImmediate(() => console.log('3'));
fs.readFile(__filename, () => console.log('4'));
Promise.resolve().then(() => console.log('5'));
process.nextTick(() => console.log('6'));
console.log('7');输出通常是:1 → 7 → 6 → 5 → 2 → 4 → 3
(具体 2 和 4 谁先取决于 poll 阶段是否阻塞,但 6 和 5 一定紧接在同步代码后)
原因不是“谁写得早”,而是:
- 同步代码先执行(1、7);
- 然后清空 nextTick 队列(6);
- 再清空微任务队列(5);
- 进入 timers 阶段执行
setTimeout(2); - poll 阶段执行文件读取回调(4);
- check 阶段执行
setImmediate(3)。
Node.js 的事件循环不是靠 JS 控制的“循环”,而是一个由 libuv 驱动的、固定节奏的状态机。理解这点,异步行为就不再玄学。


















