人人都会AI编程

6.4 异步编程范式演进:回调函数 → Promise → Generator → async/await

更新时间:2026-07-11

理解事件循环的底层机制(6.2 节)之后,我们就能明白为什么 JavaScript 必须依赖异步编程——单线程模型下,任何耗时操作都不能阻塞主线程。然而“知道为什么异步”和“怎么写好异步代码”是两回事。从早期的回调地狱到如今优雅的 async/await,JavaScript 异步编程经历了一条清晰的演进路径。理解这一段历史,不仅能帮你读懂旧代码,更能让你真正掌握现代异步编程的精髓。

6.4.1 回调函数:最原始的异步武器

回调函数是异步编程最基础的形式:把一个函数作为参数传给另一个函数,等到异步操作完成时,由后者调用这个函数。

// 模拟一个异步请求
function fetchData(callback) {
  setTimeout(() => {
    callback(null, "数据到达");
  }, 1000);
}

fetchData((err, data) => {
  if (err) {
    console.error("出错啦");
  } else {
    console.log(data); // "数据到达"
  }
});

优点:简单直接,浏览器中大量 API(setTimeoutaddEventListenerXMLHttpRequest)都使用回调。

致命缺陷:当多个异步操作前后依赖时,就会出现臭名昭著的“回调地狱”(Callback Hell),代码不断向右缩进,逻辑难以阅读和维护:

step1(function(value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // 已经不知道在哪一层了……
      });
    });
  });
});

此外,回调方式对错误处理极不友好,每一层都需要手动传 err 参数或 try-catch,极易遗漏。控制权倒置也意味着你无法信任回调是否只被调用一次、是否会在错误时机执行。

6.4.2 Promise:让异步操作可组合

ES6 引入的 Promise 彻底改变了异步编程的面貌。它将回调函数的“嵌套”结构拍平,变成链式调用。

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("数据到达");
    }, 1000);
  });
}

fetchData()
  .then(data => {
    console.log(data); // "数据到达"
    return "下一步的数据";
  })
  .then(nextData => {
    console.log(nextData); // "下一步的数据"
  })
  .catch(err => {
    console.error("错误流被统一捕获");
  });

核心价值

  • 状态不可逆:Promise 有三种状态(pending、fulfilled、rejected),一旦状态改变就不会再变,彻底解决了回调函数被多次调用或提前调用的不确定性。
  • 链式调用:每个 .then 返回一个新的 Promise,可以将多个异步步骤串联起来,逻辑沿着链条正向展开,不再向右疯狂缩进。
  • 统一错误处理:链末端的 .catch 能捕获前面任意 .then 中的错误,告别一层层手动传 err。
  • 静态方法Promise.all 处理并发,Promise.race 响应竞速,Promise.allSettled 等待所有结果(无论成功失败)——这些组合能力让复杂的异步流程控制变得优雅。

仍然存在的不足:链式调用虽然比金字塔好很多,但当异步步骤变多,代码还是会变成一长串 .then().then().then(),仍然不够接近同步代码的线性思维。此外,错误栈在 Promise 链中有时不够清晰。

6.4.3 Generator:暂停与唤醒的魔法

ES6 同时引入了 Generator 函数,它可以在函数执行过程中被“暂停”和“恢复”。这为异步编程提供了一种全新的思路:我们可以写一个看起来像同步的函数,在需要等待异步操作的地方用 yield 暂停,外部通过迭代器控制恢复时机。

function* fetchData() {
  const data = yield new Promise(resolve => {
    setTimeout(() => resolve("数据到达"), 1000);
  });
  console.log(data); // "数据到达"
}

但 Generator 本身并不自动处理异步,需要配合一个执行器(Runner)来管理 yield 后的 Promise。著名的 co 库就是干这个的:

const co = require('co');

co(function* () {
  const data = yield fetchData(); // fetchData 返回 Promise
  console.log(data);
});

Generator 的贡献:它第一次让异步代码可以用同步写法表达,也为后来的 async/await 提供了灵感。在实际业务中,Generator 目前已经不再是编写异步代码的主流选择,但它的思想仍然有价值——迭代器、生成器在数据流控制、惰性求值等场景依然活跃。

6.4.4 async/await:异步的终极语法糖

ES2017 引入的 async/await 是建立在 Promise 之上的语法糖,它直接吸收了 Generator 的“同步写法”和 Promise 的可组合性,让异步代码真正拥有了同步外观和同步的错误处理方式。

async function getData() {
  try {
    const data = await fetchData();
    console.log(data);
    const result = await processData(data);
    console.log(result);
  } catch (err) {
    console.error(err);
  }
}

关键特性

  • async 函数返回一个 Promise,可以用 .then / .catch 与其交互。
  • await 只能出现在 async 函数内部,用于等待一个 Promise 的结果,期间函数执行被暂停,不阻塞主线程。
  • 错误处理可以直接使用 try/catch,栈追踪友好,调试体验极佳。
  • 多个独立的异步操作可以并行:const [a, b] = await Promise.all([fetchA(), fetchB()]),既保持了同步写法,又不损失并发性能。

async/await 并非万能:它是 Promise 的包装,因此必须理解 Promise 才能用好它。常见的陷阱包括:忘记 await(结果拿到的是 Promise 对象本身)、在循环中不小心串行调用、错误处理遗漏等。但只要基础扎实,async/await 几乎是目前编写异步代码最舒适的方式。

6.4.5 演进路径背后的逻辑

| 范式 | 核心思路 | 解决什么问题 | 还有什么痛点 |
|------|---------|--------------|--------------|
| 回调函数 | 函数作为参数传递 | 实现基本的异步通知 | 回调地狱、错误处理困难、控制权倒置 |
| Promise | 状态机 + 链式调用 | 拍平嵌套、统一错误处理、不可逆状态 | 链式仍然不够直观,调试有时不便 |
| Generator | yield 暂停函数执行 | 用同步写法表达异步逻辑 | 需要手动执行器,侵入性强 |
| async/await | async 函数 + await 表达式 | 完美的同步写法、原生 try/catch | 建立在 Promise 之上,需理解底层 |

理解这条演进路径,不在于背诵各个时期的历史,而在于看清一个本质:JavaScript 社区一直在追求用同步的思维写异步的代码。从回调到 async/await,每一步都在拉近代码外观与开发者逻辑之间的距离,同时保留事件驱动模型非阻塞的硬实力。看懂这条脉络,你就拥有了驾驭一切异步代码的底层心智。