Node.js 事件循环阶段

解释 Node.js 事件循环的阶段顺序、各阶段职责,以及 timers、poll、check 和微任务的配合方式。

#type / concept #status / growing #tech / dev / backend #resource / nodejs #resource / javascript

[!info] related notes

Node.js 事件循环阶段

一句话定义

Node.js 事件循环不是只分“宏任务和微任务”,而是由 libuv 组织成多个阶段,再配合 process.nextTick 和 Promise 微任务共同完成调度。

如果要继续追到底层“poll 为什么重要、为什么文件 I/O 常走线程池、loop 为什么退出”,继续看 libuv 事件循环与 Worker Pool

核心机制 / 工作原理

经典阶段顺序可以先记成:

timers

pending callbacks

idle, prepare

poll

check

close callbacks

timers

  • 处理到期的 setTimeout / setInterval

pending callbacks

  • 处理某些延后到下一轮的系统级回调

poll

  • 等待并处理大部分 I/O 回调
  • 决定这一轮是继续取 I/O,还是转去 check

这一步是最容易被忽略的核心:

  • 如果此时已经有到期 timer,循环不会一直卡在 poll
  • 如果没有新的 I/O,但存在 setImmediate,也会更快转去 check
  • 如果两边都没有,就可能在这里等待新的 I/O 事件

check

  • 处理 setImmediate

close callbacks

  • 处理连接关闭等收尾回调

这个阶段模型和 libuv 的关系

这篇更偏 Node.js 用户可见的阶段视图。

如果想理解下面这些更底层问题:

  • loop alive 怎么判断
  • poll timeout 怎么算
  • 网络 I/O 和文件 I/O 为什么路径不同
  • Worker Pool 和 worker_threads 有什么边界

就要切到 libuv 事件循环与 Worker Pool

为什么 Node.js 不适合只记“宏任务 / 微任务”

因为那种模型只能解释一半问题。

在 Node.js 面试里更高频的追问其实是:

  • 为什么 setImmediatesetTimeout(fn, 0) 顺序不稳定
  • 为什么 I/O 回调里 setImmediate 更常先执行
  • 为什么 process.nextTick 会饿死事件循环

这些都需要回到“阶段模型”才能讲稳。

最小例子 / 最小场景

如果只看顶层主模块:

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

这里 timeoutimmediate 的先后不应该背死。

但如果它们出现在 I/O 回调里:

import fs from 'node:fs';

fs.readFile(__filename, () => {
  setTimeout(() => console.log('timeout'), 0);
  setImmediate(() => console.log('immediate'));
});

通常更容易先看到 immediate,因为 I/O 回调刚从 poll 阶段出来,下一步就更接近 check 阶段。

边界与易混淆点

微任务不属于上面这些阶段之一

Node.js 里还要额外记住两类高优先级队列:

  • process.nextTick
  • Promise 微任务

通常可先用这个心智模型:

同步代码

process.nextTick

Promise 微任务

再继续事件循环阶段

process.nextTick 比 Promise 还高

这也是为什么下面这段代码:

Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));

通常更容易看到:

nextTick
promise

poll 不是“固定立刻结束的一步”

它既负责取 I/O,也决定事件循环接下来是不是该等、该跳还是该继续推进。

所以很多“为什么这个回调这次先执行”的题,本质都绕不开 poll

饿死事件循环是什么意思

如果 process.nextTick 递归不断塞任务,事件循环就可能一直没机会进入后面的阶段:

function spin() {
  process.nextTick(spin);
}

spin();

这时:

  • timer 可能迟迟不执行
  • I/O 回调可能迟迟得不到处理
  • 整个服务看起来就像“活着但不干活”

不要把浏览器模型原封不动搬到 Node.js

浏览器常用“宏任务 / 微任务 / 渲染”心智模型。

Node.js 更适合记成:

多阶段事件循环 + nextTick + Promise 微任务。

最小面试回答模板

如果面试官问“Node.js 事件循环怎么理解”,可以先答:

Node.js 事件循环是 libuv 驱动的多阶段调度模型,不只是宏任务和微任务。主线程先执行同步代码,之后还要考虑 process.nextTick、Promise 微任务,以及 timerspollcheck 等阶段。像 setTimeout 属于 timerssetImmediate 属于 check,所以它们的先后顺序要结合当前是不是刚处理完 I/O 来看。

创建于 2026/5/21 更新于 2026/5/27