从”打爆电话”到”优雅等餐”:Promise 与 async/await 的前世今生

从"打爆电话"到"优雅等餐":Promise 与 async/await 的前世今生

JavaScript Async

你有没有经历过这样的代码?

getUserInfo(userId, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetails(orders[0].id, function(details) {
      getPaymentInfo(details.paymentId, function(payment) {
        // 恭喜你,已经分不清这是第几层了
        console.log(payment);
      });
    });
  });
});

这就是传说中的回调地狱——代码向右生长,像个倒过来的金字塔。

今天我们用一个故事,把 JavaScript 异步编程彻底讲明白。


一切从一个外卖订单开始

假设你在开发一个外卖 App,用户下单后,你的代码需要依次完成:

  1. 验证用户身份
  2. 检查餐厅是否营业
  3. 创建订单
  4. 扣款
  5. 通知骑手接单

每一步都要等上一步完成才能继续,而且每一步都可能失败。

这就是异步编程的经典场景:多个有依赖关系的异步操作串行执行


原始时代:回调函数

打电话的模式

在 Promise 出现之前,我们用回调函数处理异步操作。就像打电话:

你:老板,给我做个汉堡!
老板:好的,做好了打你电话!
(你挂断电话,干别的事)
老板:(做好后)喂,汉堡好了!

代码实现:

function makeBurger(callback) {
  setTimeout(() => {
    callback(null, { name: '汉堡', price: 25 });
  }, 2000);
}

makeBurger(function(err, burger) {
  if (err) {
    console.log('做失败了:', err);
    return;
  }
  console.log('收到:', burger);
});

问题来了:连锁订单

如果要连续做汉堡、薯条、可乐呢?

makeBurger(function(err, burger) {
  if (err) return handleError(err);

  makeFries(function(err, fries) {
    if (err) return handleError(err);

    makeCola(function(err, cola) {
      if (err) return handleError(err);

      // 终于集齐了...
      serveMeal([burger, fries, cola]);
    });
  });
});

问题显而易见:

  • 代码向右"金字塔化"
  • 错误处理到处重复
  • 变量作用域混乱
  • 很难在中间插入新步骤

Promise 时代:订单系统

什么是 Promise?

Promise 就像餐厅的取餐号牌

你:老板,给我做个汉堡!
老板:给你个号牌,做好了叫号!
你:(拿着号牌走了)

号牌有三种状态:

  • Pending(等待中):还没做好
  • Fulfilled(已兑现):做好了,来取餐
  • Rejected(已拒绝):卖光了,做不了

Promise 化的代码

function makeBurger() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 成功:交出汉堡
      resolve({ name: '汉堡', price: 25 });

      // 失败:拒绝订单
      // reject('卖光了!');
    }, 2000);
  });
}

链式调用:优雅的流水线

makeBurger()
  .then(burger => {
    console.log('收到汉堡:', burger);
    return makeFries();
  })
  .then(fries => {
    console.log('收到薯条:', fries);
    return makeCola();
  })
  .then(cola => {
    console.log('收到可乐:', cola);
    console.log('套餐齐了!');
  })
  .catch(err => {
    // 统一的错误处理!
    console.log('出问题了:', err);
  });

对比回调地狱,代码变成了垂直的流水线

  • 每一步清晰可见
  • 错误统一处理
  • 随时可以插入新步骤

Promise.all:并行出餐

如果要同时做三个东西,谁先做好谁先上?

Promise.all([
  makeBurger(),
  makeFries(),
  makeCola()
])
  .then(([burger, fries, cola]) => {
    // 三个都做好了,一起上桌
    serveMeal([burger, fries, cola]);
  })
  .catch(err => {
    // 任何一个失败,整个套餐取消
    console.log('有东西做失败了:', err);
  });

Promise.race:谁快用谁

点外卖时,同时看三家店,谁先接单用谁:

Promise.race([
  orderFrom('麦当劳'),
  orderFrom('肯德基'),
  orderFrom('汉堡王')
])
  .then(firstReady => {
    console.log('最先接单的是:', firstReady);
  });

async/await 时代:同步的错觉

终极优雅:像写同步代码一样写异步

Promise 链式调用已经很优雅了,但 async/await 把它推向了极致:

async function orderMeal() {
  try {
    const burger = await makeBurger();
    console.log('收到汉堡:', burger);

    const fries = await makeFries();
    console.log('收到薯条:', fries);

    const cola = await makeCola();
    console.log('收到可乐:', cola);

    console.log('套餐齐了!');
    return [burger, fries, cola];

  } catch (err) {
    console.log('出问题了:', err);
  }
}

看起来像同步代码,但实际上还是异步的!

底层原理:语法糖

async/await 本质上是 Promise 的语法糖。上面的代码等价于:

function orderMeal() {
  return makeBurger()
    .then(burger => {
      console.log('收到汉堡:', burger);
      return makeFries();
    })
    .then(fries => {
      console.log('收到薯条:', fries);
      return makeCola();
    })
    .then(cola => {
      console.log('收到可乐:', cola);
      console.log('套餐齐了!');
      return [burger, fries, cola];
    })
    .catch(err => {
      console.log('出问题了:', err);
    });
}

async 函数自动返回 Promise

async function sayHello() {
  return 'Hello!';
}

sayHello().then(msg => console.log(msg));  // 'Hello!'

await 只能在 async 函数内使用

// ❌ 错误!
const burger = await makeBurger();

// ✅ 正确
async function order() {
  const burger = await makeBurger();
}

并行优化:不要过度 await

// ❌ 串行执行,慢!
async function slowOrder() {
  const burger = await makeBurger();   // 等2秒
  const fries = await makeFries();     // 等2秒
  const cola = await makeCola();       // 等2秒
  // 总共6秒
}

// ✅ 并行执行,快!
async function fastOrder() {
  const [burger, fries, cola] = await Promise.all([
    makeBurger(),
    makeFries(),
    makeCola()
  ]);
  // 总共2秒(最慢的那个)
}

实战:完整的外卖订单流程

// 工具函数:模拟异步操作
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 各环节实现
async function verifyUser(userId) {
  await delay(500);
  if (!userId) throw new Error('用户未登录');
  return { id: userId, name: '张三' };
}

async function checkRestaurant(restaurantId) {
  await delay(300);
  const isOpen = Math.random() > 0.2;
  if (!isOpen) throw new Error('餐厅已打烊');
  return { id: restaurantId, name: '麦当劳' };
}

async function createOrder(user, restaurant) {
  await delay(800);
  return { 
    orderId: 'ORD' + Date.now(),
    userId: user.id,
    restaurantId: restaurant.id 
  };
}

async function processPayment(order) {
  await delay(1000);
  const success = Math.random() > 0.1;
  if (!success) throw new Error('支付失败');
  return { ...order, paid: true };
}

async function notifyRider(order) {
  await delay(500);
  return { ...order, riderId: 'RIDER001', status: '配送中' };
}

// 主流程
async function placeOrder(userId, restaurantId) {
  try {
    console.log('开始处理订单...');

    const [user, restaurant] = await Promise.all([
      verifyUser(userId),
      checkRestaurant(restaurantId)
    ]);

    const order = await createOrder(user, restaurant);
    const paidOrder = await processPayment(order);
    const finalOrder = await notifyRider(paidOrder);

    return finalOrder;

  } catch (error) {
    console.log('订单失败:', error.message);
    throw error;
  }
}

// 使用
placeOrder('USER001', 'REST001')
  .then(order => console.log('订单完成:', order))
  .catch(err => console.log('最终错误:', err));

总结:三代技术的选择

方式 适用场景 优点 缺点
回调函数 简单、单层异步 理解简单 多层嵌套=地狱
Promise 复杂链式调用、并行操作 链式优雅、错误统一 then 嵌套多了也乱
async/await 大部分异步场景 最优雅、最像同步 需要理解 Promise 原理

我的建议

  • 90% 场景用 async/await
  • 需要并行时用 Promise.all
  • 读别人的代码时,三者都要会

延伸:为什么 JavaScript 需要异步?

JavaScript 是单线程语言,一次只能做一件事。

如果用同步方式请求服务器:

const data = fetchSync('https://api.example.com/data');
// 在收到响应前,整个页面卡死!

所以 JavaScript 用异步:

fetch('https://api.example.com/data')
  .then(data => console.log(data));
// 请求发出后,代码继续执行

最后的思考题

async function quiz() {
  console.log(1);

  await Promise.resolve().then(() => console.log(2));

  console.log(3);

  Promise.resolve().then(() => console.log(4));

  console.log(5);
}

quiz();

输出顺序是什么?(答案在评论区揭晓)


记住:异步编程不是魔法,只是让代码在等待时"去做别的事"。理解了这一点,Promise 和 async/await 就不再是黑盒了。

下次写代码时,想象你在经营一家餐厅——让客人拿着号牌去干别的事,好了再叫号。

这就是异步编程的本质。

Views: 0