Node.js 事件循环与异步编程完全指南

小飞兽 Node.js 478 次阅读 2026-05-20

什么是事件循环?

Node.js 是基于 Chrome V8 引擎的 JavaScript 运行时,天生采用单线程 + 事件循环的架构。事件循环是 Node.js 能够用少量线程处理大量并发请求的核心所在。

与传统多线程服务器不同,Node.js 在单线程中通过事件循环不断从任务队列中取出回调执行,从而实现非阻塞 I/O 操作。

事件循环的执行顺序

Node.js 事件循环分为多个阶段(phase),每个阶段都有自己独立的 FIFO 回调队列:

1. timers — 执行 setTimeout()setInterval() 的回调
2. pending callbacks — 执行上一轮延迟的 I/O 回调
3. idle, prepare — 内部使用
4. poll — 获取新的 I/O 事件,执行 I/O 相关的回调(除 close、timers、setImmediate 外的几乎所有回调)
5. check — 执行 setImmediate() 的回调
6. close callbacks — 执行 close 事件的回调,例如 socket.on('close', ...)

宏任务与微任务

JavaScript 中的任务分为宏任务(MacroTask)和微任务(MicroTask):

  • <strong>宏任务</strong>:整体代码块、<code>setTimeout</code>、<code>setInterval</code>、<code>setImmediate</code>、<code>I/O</code>、<code>UI rendering</code>

  • <strong>微任务</strong>:<code>Promise.then()</code>、<code>Promise.catch()</code>、<code>process.nextTick()</code>、<code>queueMicrotask()</code>

执行顺序规律:同步代码 → 微任务 → (一个宏任务完成后,再次检查微任务)→ 下一个宏任务

其中 process.nextTick() 优先级高于 Promise.then()

代码示例:手写理解事件循环

// event-loop-demo.js
const fs = require('fs');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);

// async/await 函数
async function fetchData() {
  console.log('[1] async 函数开始');

  // 微任务:当前宏任务结束后立即执行
  Promise.resolve().then(() => {
    console.log('[2] Promise.then() 微任务');
  });

  // process.nextTick 比 Promise.then 优先级更高
  process.nextTick(() => {
    console.log('[3] process.nextTick() 微任务(最高优先级)');
  });

  // 同步代码
  console.log('[4] 同步代码执行');

  // 等待一个微任务
  await Promise.resolve();
  console.log('[7] await 之后的代码(属于下一个微任务)');

  return 'done';
}

// setImmediate vs setTimeout 演示
setImmediate(() => {
  console.log('[5] setImmediate (check 阶段)');
});

setTimeout(() => {
  console.log('[6] setTimeout (timers 阶段,延迟 0ms)');
}, 0);

console.log('[0] 主程序同步代码开始');

fetchData().then(() => {
  console.log('[8] async 函数 Promise resolved');
});

// 最终输出顺序:
// [0] 主程序同步代码开始
// [1] async 函数开始
// [4] 同步代码执行
// [3] process.nextTick() 微任务(最高优先级)
// [2] Promise.then() 微任务
// [7] await 之后的代码
// [6] setTimeout (timers 阶段)
// [5] setImmediate (check 阶段)
// [8] async 函数 Promise resolved

async/await 进阶用法

// async-await-advanced.js

// 并发执行多个请求(Promise.all)
async function fetchAll() {
  const [users, posts] = await Promise.all([
    fetch('https://jsonplaceholder.typicode.com/users').then(r => r.json()),
    fetch('https://jsonplaceholder.typicode.com/posts').then(r => r.json()),
  ]);
  console.log(`获取到 ${users.length} 个用户,${posts.length} 篇文章`);
  return { users, posts };
}

// 错误处理
async function safeFetch(url) {
  try {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error('请求失败:', err.message);
    return null;
  }
}

// 顺序执行 vs 并发执行对比
async function sequential() {
  // 串行:每个请求等待前一个完成,总耗时 = sum(各请求耗时)
  const r1 = await fetch('/api/a').then(r => r.json());
  const r2 = await fetch('/api/b').then(r => r.json());
  const r3 = await fetch('/api/c').then(r => r.json());
  return [r1, r2, r3];
}

async function parallel() {
  // 并发:所有请求同时发送,总耗时 = max(各请求耗时)
  const [r1, r2, r3] = await Promise.all([
    fetch('/api/a').then(r => r.json()),
    fetch('/api/b').then(r => r.json()),
    fetch('/api/c').then(r => r.json()),
  ]);
  return [r1, r2, r3];
}

常见问题

Q1: 为什么 setTimeout(fn, 0) 不一定在 setImmediate 之前执行?

在 Node.js 中,当 I/O 循环内调用时,setImmediate 的回调总是在 poll 阶段的 setTimeout 回调之前执行,因为 setImmediate 本身就是 check 阶段。但从主模块直接运行时,两者的顺序不确定,取决于进程启动时性能的影响。

Q2: <code>process.nextTick()</code> 和 <code>queueMicrotask()</code> 有什么区别?

两者都是微任务,但 process.nextTick() 属于 Node.js 特有的 API,会被放入一个独立的队列(nextTickQueue),在每次切换到下个阶段前清空;而 queueMicrotask() 放入微任务队列(microTaskQueue),受 V8 引擎管理。process.nextTick() 优先级更高,但可能导致微任务堆积,建议仅在必要时使用。

Q3: async/await 和 Promise.then() 哪个性能更好?

两者本质相同,性能差异可以忽略。async/await 是 Promise 的语法糖,优势在于代码可读性更高、调试更方便(可以在断点处逐步执行)。在高并发场景下,避免在循环中大量使用 await(导致串行化),改用 Promise.all() 批量并发处理。

延伸阅读

  • <a href="https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/">Node.js 官方文档 - 事件循环</a>
  • <a href="https://nodejs.org/zh-cn/docs/guides/asynchronous-programming/">Node.js 异步编程的正确姿势</a>
  • 《深入浅出 Node.js》— 朴灵著