Node.js 事件循环机制

2026-06-22 · 6 阅读 · 232字
JavaScriptNode.js

Node.js 事件循环机制

事件循环概述

Node.js 的事件循环是其非阻塞 I/O 模型的核心。尽管 JavaScript 是单线程的,事件循环使得 Node.js 可以高效处理数千并发连接。

事件循环的阶段

事件循环按顺序执行以下阶段(phase):

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │      close callbacks      │
│  └───────────────────────────┘

1. timers 阶段

执行 setTimeout()setInterval() 的回调。计时器指定的是最小延迟时间,而非精确执行时间。

setTimeout(() => {
    console.log('Timer callback');
}, 0);

2. pending callbacks 阶段

执行推迟到下一轮循环的 I/O 回调,如某些系统错误回调。

3. idle, prepare 阶段

内部使用。

4. poll 阶段

事件循环的核心阶段。主要做两件事:

  1. 计算阻塞时间
  2. 处理 I/O 事件队列中的回调

如果 poll 队列不为空,同步执行回调直到队列清空或达到硬限制。如果队列为空,则检查是否有 setImmediate() 或已到期的计时器。

5. check 阶段

执行 setImmediate() 的回调。它在 poll 阶段空闲后立即执行:

setImmediate(() => {
    console.log('Immediate callback');
});

6. close callbacks 阶段

执行关闭事件的回调,如 socket.on('close', ...)

微任务与宏任务

在每次切换阶段时,事件循环会处理微任务队列。微任务的优先级高于宏任务(即事件循环阶段的回调)。

微任务

  • process.nextTick()(优先级最高)
  • Promise 的 .then() / .catch() / .finally()

宏任务(即事件循环各阶段的回调)

  • setTimeout / setInterval
  • setImmediate
  • I/O 回调
console.log('1');

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

Promise.resolve().then(() => console.log('3'));

process.nextTick(() => console.log('4'));

console.log('5');

// 输出: 1, 5, 4, 3, 2

process.nextTick 与 setImmediate

  • process.nextTick() 在当前操作完成后立即执行,属于微任务
  • setImmediate() 在事件循环的下一个 check 阶段执行

虽然 process.nextTick() 语义更直观,但过度使用会导致 I/O 饥饿。推荐优先使用 setImmediate()

常见误区

误区 1:setTimeout(fn, 0) 立即执行

setTimeout(fn, 0) 至少有 1ms 的延迟。在事件循环中,它在 timers 阶段执行,晚于同步代码和微任务。

误区 2:事件循环是单线程的

JavaScript 回调在主线程执行,但 Node.js 内部(libuv)使用线程池处理文件 I/O、DNS 查询等操作。

误区 3:所有异步操作在同一个阶段完成

不同类型的异步操作在事件循环的不同阶段处理,理解各阶段的顺序才能准确预测代码执行顺序。