Node.js 的事件循环

深入理解 Node.js 事件循环的六个阶段、setImmediate 与 setTimeout 的区别、以及 process.nextTick 的特殊机制。


Node.js 事件循环 vs 浏览器事件循环

浏览器和 Node.js 都有事件循环,但实现上有显著差异。

浏览器的事件循环围绕"渲染"展开,每次循环迭代会尝试渲染页面(requestAnimationFrame)。

Node.js 的事件循环围绕"I/O 操作"展开,更细分,有明确的六个阶段。


六个阶段详解

timers 阶段

执行 setTimeout()setInterval() 设置的回调。

setTimeout(() => console.log('timer callback'), 100);

pending callbacks 阶段

执行上一轮延迟到本轮的 I/O 回调。

idle, prepare 阶段

仅供内部使用。

poll 阶段

获取新的 I/O 事件;执行与 I/O 相关的回调。

  • 如果 poll 队列非空,依次执行回调
  • 如果 poll 队列为空,如果有 setImmediate 回调,跳到 check 阶段
  • 如果既没有 setImmediate 也没有回调,等待新的 I/O 事件

check 阶段

setImmediate() 设置的回调在此执行。

setImmediate(() => console.log('immediate'));

close callbacks 阶段

执行关闭回调,如 socket 关闭事件。


setTimeout vs setImmediate

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

在 REPL 环境和普通脚本中,输出顺序不确定——取决于系统性能。

但在 I/O 操作的回调里,setImmediate 总是先执行:

const fs = require('fs');

fs.readFile('/etc/hostname', () => {
  setTimeout(() => console.log('timeout'), 0);
  setImmediate(() => console.log('immediate'));
  process.nextTick(() => console.log('nextTick'));
});

输出顺序:nextTick, immediate, timeout

原因:I/O 回调里 poll 阶段完成后,setImmediate 进入 check 阶段立即执行,而 setTimeout 需要等 timers 阶段。


process.nextTick

process.nextTick 是 Node.js 独有的 API,它的回调优先级高于其他微任务。

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

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

输出:nextTick, promise

process.nextTick 的回调存在一个单独的队列,在每个阶段结束时(但在微任务队列之前)执行。


Node.js 中的微任务

Node.js 的微任务队列有两个优先级:

  1. nextTick 队列process.nextTick() 的回调,最高优先级
  2. 微任务队列Promise.then().catch().finally() 的回调
process.nextTick(() => console.log('1'));
Promise.resolve().then(() => console.log('2'));
queueMicrotask(() => console.log('3'));
process.nextTick(() => console.log('4'));

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

常见的 Node.js 异步模式

错误处理的约定

const fs = require('fs').promises;

async function readConfig() {
  try {
    const data = await fs.readFile('config.json', 'utf-8');
    return JSON.parse(data);
  } catch (err) {
    console.error('Failed to read config:', err);
    throw err;
  }
}

并行 I/O 操作

const { promises: fs } = require('fs');

async function readAllFiles(files) {
  // 并行读取所有文件
  const filePromises = files.map(file => fs.readFile(file, 'utf-8'));
  return Promise.all(filePromises);
}

这一章想说的

Node.js 的事件循环分为六个阶段:timers → pending callbacks → idle/prepare → poll → check → close callbacks。

关键知识点:

  • setTimeout 在 timers 阶段执行,setImmediate 在 check 阶段执行
  • 在 I/O 回调中,setImmediate 几乎总是先于 setTimeout 执行
  • process.nextTick 优先级高于普通微任务

延展阅读