三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE

异步编程:回调到 async/await 的进化

为什么会有回调地狱?Promise 解决了什么?async/await 又赢在哪里?三段代码对比,看清异步编程的进化。

异步编程:回调到 async/await 的进化

异步编程是 JavaScript 学习路上的一道坎。很多人能背出"回调 → Promise → async/await"这个进化路线,却说不清每一步到底解决了什么问题。这篇日记我用同一个需求——"先查用户、再查订单、再查物流"——分别用三种方式实现,让你直观看到进化的动力和方向。

异步编程代码对比与笔记

一、为什么需要异步

JavaScript 是单线程的,只有一个调用栈。如果一段代码要等网络请求、读文件、定时器,它就会阻塞后面所有代码。所以这些"慢操作"都被设计成异步:你先发起,我继续干别的,等结果回来再处理。问题来了——结果回来之后,下一步该怎么写?这就是异步编程要解决的核心问题。

二、第一段:回调地狱

最早的方案是回调函数:你把"结果回来后要做的事"作为函数传进去,异步操作完成时调用它。听起来没毛病,但当你要串起三步异步操作时,代码就开始向右"嵌套"了:

getUser(userId, function(user) {
  getOrders(user.id, function(orders) {
    getLogistics(orders[0].id, function(logistics) {
      console.log(logistics);
    }, function(err) {
      console.error("物流查询失败", err);
    });
  }, function(err) {
    console.error("订单查询失败", err);
  });
}, function(err) {
  console.error("用户查询失败", err);
});

这就是经典的"回调地狱"(Callback Hell)。它有三个痛点:嵌套深导致阅读要"右倾"、错误处理散落在每一层里、控制流(比如并行、超时)几乎无法表达。维护这种代码是一场灾难。

三、第二段:Promise 改进

Promise 把"未来的结果"抽象成一个对象,提供 then 链式调用和统一的 catch 错误处理。同样的需求,用 Promise 写出来扁平得多:

getUser(userId)
  .then(user => getOrders(user.id))
  .then(orders => getLogistics(orders[0].id))
  .then(logistics => {
    console.log(logistics);
  })
  .catch(err => {
    console.error("查询失败", err);
  });

Promise 解决了回调地狱的两个核心痛点:嵌套被拍平成了链,错误处理统一到 catch。它还提供了 Promise.all、Promise.race 这些组合方法,让并行、超时变得可表达。

但 Promise 也不是完美的:链式调用里每一步都是 then,逻辑稍微复杂就还是要在 then 里嵌套;而且 then 里拿到的值是被"包"过的,调试时断点不如同步代码直观。

Promise 拍平了嵌套,但没消除"异步感"。代码还是一眼能看出"这里是异步",读起来和同步代码不一样。

四、第三段:async/await 最终形态

async/await 是 Promise 的语法糖,它让你能用同步的写法写异步代码。同样的需求,第三版长这样:

async function showLogistics(userId) {
  try {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    const logistics = await getLogistics(orders[0].id);
    console.log(logistics);
  } catch (err) {
    console.error("查询失败", err);
  }
}

对比一下:try/catch 是同步代码的错误处理方式,这里直接复用,不用学新概念;每一行 await 拿到的就是真实值,不是被包过的 Promise;代码结构和同步代码一模一样,可读性拉满。

需要注意的是,async/await 底层仍然是 Promise,它没有引入新的并发模型。所以并发请求还是要用 Promise.all,不能傻傻地连续 await:

// 串行:慢,三个请求总耗时相加
const a = await fetchA();
const b = await fetchB();

// 并行:快,两个请求同时发
const [a, b] = await Promise.all([fetchA(), fetchB()]);

五、进化的本质:让异步读起来像同步

把三段放一起看,进化的方向非常清晰:让异步代码越来越像同步代码。回调把"未来要做的事"写成嵌套函数,Promise 把它拍平成链,async/await 让它看起来就是普通的同步语句。人脑天生更擅长读顺序执行的代码,所以这个进化方向是顺势而为。

但请记住,async/await 不是 Promise 的替代,而是它的语法糖。理解 Promise 是基础,async/await 只是让你用更顺手的方式表达同样的逻辑。所以学异步的正确顺序是:先彻底搞懂 Promise,再用 async/await 写得更舒服。跳过 Promise 直接学 async/await,遇到并发、错误传播、循环 await 就会懵。

这就是异步编程的进化。看懂这三段对比,你对 JavaScript 异步的理解就立住了。

← 返回学习日记