Skip to content
imshengli blog
Go back

ES6 Async/Await:异步编程的终极方案

从回调地狱到 async/await,异步编程的终极形态:语法、错误处理、并发控制与常见陷阱。

· 6 min

概述

async/await 是 ES2017 引入的异步编程语法,本质上是 Generator 函数的语法糖。它让异步代码的书写方式接近同步代码,大幅提升了可读性。

异步编程的演进

回调地狱

最早的异步处理方式是回调函数,多层嵌套后代码难以维护:

getUser(userId, function (user) {
  getOrders(user.id, function (orders) {
    getOrderDetail(orders[0].id, function (detail) {
      console.log(detail);
      // 继续嵌套...
    });
  });
});

Promise 链式调用

Promise 通过链式调用解决了嵌套问题,但仍然不够直观:

getUser(userId)
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetail(orders[0].id))
  .then(detail => console.log(detail))
  .catch(err => console.error(err));

async/await

async/await 让异步代码读起来像同步代码一样自然:

async function getDetail(userId) {
  const user = await getUser(userId);
  const orders = await getOrders(user.id);
  const detail = await getOrderDetail(orders[0].id);
  console.log(detail);
}

基本语法

async 函数声明

async 关键字用于声明一个异步函数。async 函数总是返回一个 Promise:

// 函数声明
async function fetchData() {
  return 'hello';
}

// 箭头函数
const fetchData = async () => {
  return 'hello';
};

// 返回值自动包装为 Promise
fetchData().then(val => console.log(val)); // 'hello'

// 等价于
function fetchData() {
  return Promise.resolve('hello');
}

await 表达式

await 只能在 async 函数内部使用,它会暂停函数执行,等待 Promise 完成并返回结果:

async function demo() {
  // await 等待 Promise resolve,拿到结果值
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
}

await 后面如果不是 Promise,会被自动包装成 Promise.resolve()

async function demo() {
  const val = await 42; // 等价于 await Promise.resolve(42)
  console.log(val); // 42
}

错误处理

try/catch

async/await 最大的优势之一是可以用标准的 try/catch 处理异步错误:

async function fetchUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    const user = await response.json();
    return user;
  } catch (error) {
    console.error('获取用户失败:', error.message);
    return null;
  }
}

统一处理多个异步操作的错误

async function processOrder(orderId) {
  try {
    const order = await getOrder(orderId);
    const payment = await processPayment(order);
    const receipt = await sendReceipt(payment);
    return receipt;
  } catch (error) {
    // 任何一步失败都会被捕获
    console.error('订单处理失败:', error);
    throw error;
  }
}

不用 try/catch 的替代方案

对于不想用 try/catch 的场景,可以在 await 后直接 .catch()

async function fetchData() {
  const data = await fetch('/api/data')
    .then(res => res.json())
    .catch(err => null); // 失败时返回 null

  if (!data) {
    console.log('请求失败,使用默认值');
  }
  return data;
}

并发控制

Promise.all:并行执行

当多个异步操作之间没有依赖关系时,应该并行执行:

// 错误:串行执行,浪费时间
async function getPageData() {
  const user = await fetchUser();      // 等 1 秒
  const posts = await fetchPosts();    // 再等 1 秒
  const comments = await fetchComments(); // 再等 1 秒
  // 总共 3 秒
}

// 正确:并行执行
async function getPageData() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ]);
  // 总共约 1 秒(取决于最慢的那个)
}

Promise.allSettled:全部完成不论成败

async function batchFetch(urls) {
  const results = await Promise.allSettled(
    urls.map(url => fetch(url).then(r => r.json()))
  );

  const succeeded = results
    .filter(r => r.status === 'fulfilled')
    .map(r => r.value);

  const failed = results
    .filter(r => r.status === 'rejected')
    .map(r => r.reason);

  return { succeeded, failed };
}

控制并发数量

当需要限制同时进行的请求数时:

async function asyncPool(limit, items, fn) {
  const results = [];
  const executing = [];

  for (const item of items) {
    const p = fn(item);
    results.push(p);

    if (items.length >= limit) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }

  return Promise.all(results);
}

// 最多同时 3 个请求
await asyncPool(3, urls, url => fetch(url));

与 Generator 的关系

async/await 本质上是 Generator + 自动执行器的语法糖:

// Generator 写法(需要手动执行器)
function* fetchDataGen() {
  const response = yield fetch('/api/data');
  const data = yield response.json();
  return data;
}

// async/await 写法(内置执行器)
async function fetchData() {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
}

对应关系:

常见陷阱

循环中的 await

在循环中使用 await 会导致串行执行:

// 问题:串行执行,每次循环都要等上一个完成
async function downloadAll(urls) {
  const results = [];
  for (const url of urls) {
    const data = await fetch(url); // 一个接一个
    results.push(await data.json());
  }
  return results;
}

// 解决:先发起所有请求,再统一等待
async function downloadAll(urls) {
  const promises = urls.map(url => fetch(url).then(r => r.json()));
  return Promise.all(promises);
}

forEach 中的 await 不生效

forEach 不会等待异步回调完成:

// 错误:forEach 不支持 async
[1, 2, 3].forEach(async (num) => {
  await doSomething(num); // 不会按顺序执行
});

// 正确:使用 for...of
for (const num of [1, 2, 3]) {
  await doSomething(num); // 串行执行
}

// 或者并行执行
await Promise.all([1, 2, 3].map(num => doSomething(num)));

忘记 await

async function save(data) {
  // 错误:忘记 await,函数不会等待写入完成
  writeToDatabase(data);

  // 正确
  await writeToDatabase(data);
}

不必要的 async

如果函数只是简单返回一个 Promise,不需要 async:

// 多余的 async
async function getUser(id) {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

// 简洁写法
function getUser(id) {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

小结

async/await 是 Generator 函数的语法糖,核心价值在于:

  1. 用同步的写法处理异步逻辑,代码更直观
  2. 原生支持 try/catch 错误处理
  3. 调试体验更好(堆栈信息更清晰)

使用建议:


Share this post on:

Previous Post
Redux 状态管理:原则、数据流与中间件机制
Next Post
ES6 Generator:yield、惰性求值与异步控制