人人都会AI编程

六大阶段详解:定时器、待处理回调、轮询、检查、关闭回调

更新时间:2026-07-11

Node.js 的事件循环并不是一个单一的死循环,而是由 六个阶段 构成的顺序化轮询流程。每个阶段负责处理一种特定类型的事件回调,阶段之间会检查并清空微任务队列。完整理解这六个阶段,是诊断异步代码执行顺序、避免延迟和饥饿问题的关键。

3.3.1 阶段总览

事件循环每一轮迭代都会依次经过以下阶段:

   ┌───────────────────────────┐
   │           timers          │  setTimeout/ setInterval 回调
   └─────────────┬─────────────┘
   ┌─────────────┴─────────────┐
   │    pending callbacks      │  I/O 延迟到下一轮的回调
   └─────────────┬─────────────┘
   ┌─────────────┴─────────────┐
   │     idle, prepare         │  内部使用,开发不可见
   └─────────────┬─────────────┘
   ┌─────────────┴─────────────┐
   │          poll             │  核心 I/O 回调、等待新事件
   └─────────────┬─────────────┘
   ┌─────────────┴─────────────┐
   │          check            │  setImmediate 回调
   └─────────────┬─────────────┘
   ┌─────────────┴─────────────┐
   │     close callbacks       │  socket.close 等关闭回调
   └───────────────────────────┘

在进入每个阶段前,Node.js 会检查是否还有存活的任务或定时器。如果不再有任何未处理的事件,循环退出,进程结束。每当阶段切换时,微任务队列process.nextTickPromise)会被全部清空。

3.3.2 timers 阶段(定时器)

该阶段专门执行由 setTimeoutsetInterval 注册的、已经到期的回调函数。Node.js 在进入 timers 阶段时,会获取当前时间,然后将所有“期望触发时间 ≤ 当前时间”的定时器回调依次执行。

核心特征与陷阱

  • 定时器的延迟时间并非精确值,而是“至少延迟”。如果事件循环正阻塞在其他阶段,定时器就会被推迟执行。
  • 主线程在同一阶段内会尽量执行所有已到期的定时器回调,但如果回调之间让出了控制权(如异步 I/O 回调),也可能在下一次 timers 阶段才处理。
  • setInterval 的回调会按照设定的间隔重复执行,但如果某个循环内的执行时间超过了间隔时间,后续回调会立即排队,可能导致连续无间隔调用。
console.log('脚本开始');

setTimeout(() => console.log('timeout 1'), 0);
setTimeout(() => console.log('timeout 2'), 0);

console.log('脚本结束');
// 输出顺序通常为:脚本开始 → 脚本结束 → timeout 1 → timeout 2

3.3.3 pending callbacks 阶段(待处理回调)

这个阶段执行一些被延迟到下一轮循环的操作系统级回调。最常见的例子是某些 TCP 错误事件或 I/O 操作完成的回调,因为主线程当时正在执行其他任务,它们被挂起并安排在 pending callbacks 阶段重新执行。

日常开发中很少需要直接关注该阶段,因为它处理的往往是 libuv 内部维护的作业,比如:

  • 管道连接时的错误通知(ECONNREFUSED 等)
  • 某些系统事件的延迟触发

这部分回调并不包含由 setImmediatesetTimeout 安排的任务,也不是普通的 fs.readFile 回调(那些通常在 poll 阶段处理)。

3.3.4 idle, prepare 阶段(空闲、预备)

这是 Node.js 内部使用的两个阶段,不暴露给用户代码。idle 阶段用于执行一些内部任务,prepare 阶段则为后续的 poll 阶段做准备。开发者无法直接在此阶段注册回调,可以完全忽略它们,只需知道这两个阶段占据了一点时间花销即可。

3.3.5 poll 阶段(轮询)

poll 阶段是整个事件循环的心脏,负责两件核心事情:

  1. 执行已就绪的 I/O 回调

当 I/O 操作(如文件读取、数据库查询、网络请求)完成时,其回调函数会被推入 poll 队列。Node.js 在这个阶段会同步执行这些回调,直到队列为空或达到系统相关的上限。

  1. 决定是否阻塞并等待新的 I/O 事件

若 poll 队列已空,且没有 setImmediate 回调或到期的定时器,事件循环会在 poll 阶段阻塞,等待新的 I/O 事件(如新连接、数据到达)。一旦有事件到来,循环立即被唤醒,执行对应回调。如果脚本没有注册任何 I/O 监听器,程序就会在此处退出。

poll 阶段的阻塞行为决定了 Node.js 的高效 I/O 吞吐:它在空闲时休眠,有活时立刻响应。

例外情况

  • 如果有 setImmediate 回调在等待,poll 阶段不会阻塞,而是直接进入 check 阶段。
  • 如果定时器即将到期,poll 阶段会根据最近的到期时间设定一个超时,确保不会错过 timers 阶段。

3.3.6 check 阶段(检查)

check 阶段专门执行 setImmediate 注册的回调。setImmediate 的作用是让回调在 poll 阶段结束后立即执行,而不会阻塞在 I/O 等待中。

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

// 输出顺序在某些情况下可能是 timeout → immediate,也可能是 immediate → timeout,
// 取决于事件循环启动时的计时器注册顺序和系统时间精度。

实践中,如果代码是在一个 I/O 回调内部(如 fs.readFile 的回调)同时注册 setTimeout(fn, 0)setImmediate(fn),那么 setImmediate 一定会先执行。因为 I/O 回调完成后事件循环进入 poll 阶段,而在 poll 阶段发现有 setImmediate 等待,就会跳过阻塞直接进入 check 阶段;而 setTimeout 的回调要等到下一轮 timers 阶段才被执行。

3.3.7 close callbacks 阶段(关闭回调)

当 socket 或句柄因关闭事件(如 socket.destroy()fs.close())而需要执行回调时,这些回调会在 close callbacks 阶段集中处理。典型的如 socket.on('close', ...) 就会被安排到这里。

const net = require('net');
const server = net.createServer((socket) => {
  socket.on('close', () => console.log('连接关闭'));
});
server.listen(8080);

这个阶段执行完毕后,事件循环的一轮迭代结束,会检查是否还有活跃的事件或定时器。如果有,则进入下一轮循环;否则进程退出。

3.3.8 微任务插入点:nextTickPromise

虽然微任务不属于六大阶段中的任何一个,但它们对理解执行顺序至关重要。在事件循环的每个阶段转换时(即一个阶段完成,准备进入下一个阶段前),Node.js 会清空 微任务队列,包括:

  • process.nextTick 回调(nextTick 队列优先级最高)
  • Promise.thenasync/await 的完成回调(microtask 队列)

这意味着 process.nextTick 总在当前操作结束后、任意其他异步 I/O 或定时器回调之前执行。过度使用 nextTick 可能导致 I/O 饥饿,因为事件循环始终无法进入后续阶段。因此,通常情况下应优先使用 setImmediatesetTimeout 来让出控制权。

console.log('start');
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
setImmediate(() => console.log('immediate'));
console.log('end');

// 输出:
// start
// end
// nextTick
// promise
// immediate

掌握六大阶段的运行流程与微任务插入点,就能够精确预测任意异步组合的执行顺序,也更有信心在调试性能瓶颈时定位事件循环阻塞的根源。在实际项目中,这份知识是排查“为何我的定时器不准”“为何 setImmediate 比 setTimeout 先执行”等经典问题的基石。