Skip to content

第 3 章:异步编程深入 ​

异步是 Node 的灵魂。这一章梳理回调 → Promise → async/await 的演进脉络,再讲事件机制和错误处理,让你写出干净、可控的异步代码。


3.1 回调函数:一切的起点 ​

Node 最早的异步写法是回调函数——I/O 完成后调用你传入的函数:

javascript
const fs = require('fs');

// 读文件:回调接收 (err, data)
fs.readFile('/etc/hosts', 'utf8', (err, data) => {
  if (err) { console.error('读失败', err); return; }
  console.log(data);
});

问题:多个异步操作串行时,嵌套越来越深,就是著名的「回调地狱」:

javascript
fs.readFile('a.txt', (err, a) => {
  fs.readFile('b.txt', (err, b) => {
    fs.readFile('c.txt', (err, c) => {
      // 三层嵌套,再往下就没法看了
    });
  });
});

3.2 Promise:把回调「拉平」 ​

Promise 是一个「未来才会完成」的对象,有三种状态,状态一旦改变就不可逆:

          ┌───────── pending(进行中)─────────┐
          │                │                   │
          ▼                ▼                   ▼
   fulfilled(成功)   rejected(失败)      (终态,不可再变)
      .then()            .catch()

用 Promise 改写上面的链式读取:

javascript
const fs = require('fs/promises');

fs.readFile('a.txt', 'utf8')
  .then(a => fs.readFile('b.txt', 'utf8'))   // 返回新 Promise,链式串联
  .then(b => fs.readFile('c.txt', 'utf8'))
  .then(c => console.log(c))
  .catch(err => console.error('出错', err)); // 一处捕获所有错误

关键规则:

  • .then() 永远返回一个新 Promise,所以能一直 .then().then() 链下去;
  • 链上任何一环 reject,都会跳到最近的 .catch()。

3.3 async/await:异步写起来像同步 ​

async/await 是 Promise 的语法糖,让异步代码看起来像同步,可读性最好:

javascript
const fs = require('fs/promises');

async function readAll() {
  try {
    const a = await fs.readFile('a.txt', 'utf8'); // 等待,不阻塞事件循环
    const b = await fs.readFile('b.txt', 'utf8');
    const c = await fs.readFile('c.txt', 'utf8');
    return a + b + c;
  } catch (err) {
    console.error('出错', err);
  }
}

注意一个常见误区:await 是「暂停当前 async 函数」,不是「阻塞整个线程」——await 期间事件循环继续处理其他任务。

演进对比一览:

回调        嵌套深、难读、易漏错 ──► 早期
Promise     拉平嵌套、统一错误    ──► 主流
async/await 最像同步、最易读      ──► 现代首选

实践建议:新代码一律 async/await;Promise.all 并行多个独立异步任务;只在万不得已时写回调(如某些老库)。


3.4 并发:Promise.all 并行执行 ​

多个互不依赖的异步任务,用 Promise.all 并行,而不是串行 await:

javascript
// ❌ 串行:总耗时 = 三者之和
const a = await fetchDataA();
const b = await fetchDataB();
const c = await fetchDataC();

// ✅ 并行:总耗时 ≈ 最慢的那个
const [a, b, c] = await Promise.all([
  fetchDataA(),
  fetchDataB(),
  fetchDataC(),
]);
串行:  A ──► B ──► C        (3 个单位时间)
并行:  A ─┐
        B ─┼──► 汇总         (1 个单位时间)
        C ─┘

3.5 事件机制:EventEmitter ​

除了回调/Promise,Node 还有一种异步范式——事件驱动。EventEmitter 让你发布/订阅事件:

javascript
const { EventEmitter } = require('events');

const emitter = new EventEmitter();

// 订阅:监听 'data' 事件
emitter.on('data', (chunk) => {
  console.log('收到数据块', chunk);
});

// 发布:触发 'data' 事件(可多次)
emitter.emit('data', 'hello');
emitter.emit('data', 'world');

典型应用:http.Server、fs.ReadStream 都是 EventEmitter,所以能 server.on('request', ...)、stream.on('data', ...)。

on 和 once 的区别:on 每次事件都触发,once 只触发一次。


3.6 错误处理三原则 ​

异步错误处理最容易漏,记住三条:

  1. Promise 链末尾一定有 .catch,或 async 函数里 try/catch;
  2. 回调风格:约定 err 作为第一个参数,先判断 if (err);
  3. async 函数:它永远返回 Promise,调用方要用 await 或 .catch,否则错误会静默丢失。
javascript
// ❌ 危险:错误没人接,变成 unhandled rejection
someAsyncTask();

// ✅ 安全
someAsyncTask().catch(err => console.error(err));

3.7 本章小结 ​

要点说明
演进脉络回调(易嵌套)→ Promise(拉平)→ async/await(首选)
Promise 状态pending → fulfilled/rejected,不可逆
并行独立任务用 Promise.all 并行,别串行 await
事件EventEmitter 发布/订阅,是 Node 内置对象的基石
错误处理catch 别漏,async 函数要接住

下一章预告 ​

异步模型掌握了,该上手 Node 自带的武器库了——fs、path、http、stream 四大核心模块,日常开发绕不开。

👉 第 4 章:核心模块实战

📖本文阅读--次|📊全站访问--次|👥访客--人