JavaScript 是单线程语言,但 Web 应用却需要同时处理网络请求、文件读取、定时器等耗时操作。异步编程一直是 JavaScript 的核心挑战。从回调函数到 Promise,再到 async/await,JavaScript 异步编程的演进让代码变得越来越清晰、越来越容易维护。

回调地狱:问题根源

在 Promise 出现之前,异步操作通过回调函数处理。当多个异步操作需要按顺序执行时,代码会形成深层嵌套,俗称"回调地狱"(Callback Hell):

// 回调地狱:难以阅读、难以维护
getUser(userId, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetail(orders[0].id, function(detail) {
      getComments(detail.productId, function(comments) {
        console.log(comments);
        // 如果还有下一步,嵌套继续加深...
      });
    });
  });
});

这种金字塔式的缩进不仅让代码难以阅读,还导致错误处理极为繁琐——每一步都需要单独处理错误,稍有不慎就会遗漏。

Promise 基础

Promise 是一个代表异步操作最终完成或失败的对象。它有三种状态:pending(进行中)、fulfilled(已完成)和 rejected(已拒绝)。状态一旦改变就不可逆转。

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: '张三' });
      } else {
        reject(new Error('无效的用户 ID'));
      }
    }, 1000);
  });
}

// 使用 Promise
fetchUser(1)
  .then(user => {
    console.log(user); // { id: 1, name: '张三' }
  })
  .catch(err => {
    console.error(err.message);
  })
  .finally(() => {
    console.log('请求完成'); // 无论成功失败都会执行
  });

Promise 链式调用

Promise 的 .then() 方法返回一个新的 Promise,这使得链式调用成为可能。每个 .then() 接收上一个 Promise 的返回值,将回调地狱拉平为线性结构:

fetchUser(1)
  .then(user => fetchOrders(user.id))
  .then(orders => getOrderDetail(orders[0].id))
  .then(detail => getComments(detail.productId))
  .then(comments => {
    console.log('评论列表:', comments);
  })
  .catch(err => {
    // 统一错误处理,任何一步失败都会跳到这里
    console.error('出错了:', err.message);
  });

链式调用中,.catch() 会捕获链上任意一步的错误,再也不用像回调时代那样为每一步单独写错误处理。如果需要在特定步骤做局部错误处理,可以插入一个 .catch(),它返回的 Promise 会继续向后传递。

Promise 组合方法

当多个异步操作之间没有依赖关系,可以并行执行时,Promise 提供了几个静态方法来统一管理:

const p1 = fetch('/api/users');
const p2 = fetch('/api/products');
const p3 = fetch('/api/orders');

// Promise.all:全部成功才成功,一个失败就失败
Promise.all([p1, p2, p3])
  .then(([users, products, orders]) => {
    console.log('全部加载完成');
  })
  .catch(err => {
    console.error('其中一个请求失败:', err);
  });

// Promise.allSettled:等待全部完成,无论成功失败
Promise.allSettled([p1, p2, p3])
  .then(results => {
    results.forEach(result => {
      if (result.status === 'fulfilled') {
        console.log('成功:', result.value);
      } else {
        console.log('失败:', result.reason);
      }
    });
  });

// Promise.race:取最快完成的那个
Promise.race([p1, p2, p3])
  .then(fastest => {
    console.log('最快的响应:', fastest);
  });

选择哪个方法取决于业务需求:需要全部成功用 Promise.all,需要知道每个结果用 Promise.allSettled,需要竞速用 Promise.race。ES2020 还引入了 Promise.any(),它只要有一个成功就返回成功值。

Async/Await

async/await 是 Promise 的语法糖,让异步代码看起来和同步代码几乎一模一样。用 async 关键字声明异步函数,用 await 关键字等待 Promise 完成:

async function loadComments(userId) {
  try {
    const user = await fetchUser(userId);
    const orders = await fetchOrders(user.id);
    const detail = await getOrderDetail(orders[0].id);
    const comments = await getComments(detail.productId);
    console.log('评论列表:', comments);
    return comments;
  } catch (err) {
    console.error('出错了:', err.message);
    throw err; // 继续向上抛出
  }
}

对比 Promise 链式写法,async/await 的优势在于:代码结构是线性的,从上到下依次执行,阅读体验与同步代码一致;try/catch 可以统一处理整段逻辑中的错误,比 .catch() 更自然;调试时可以在 await 语句上设置断点,而 .then() 回调中的断点往往不太方便。

错误处理的最佳实践

async/await 中使用 try/catch 处理错误,但并非每一步都需要 try/catch。一个常见的模式是只在最外层统一捕获:

async function loadPage() {
  try {
    const [users, posts] = await Promise.all([
      fetchUsers(),
      fetchPosts()
    ]);
    renderPage(users, posts);
  } catch (err) {
    showError(err.message);
  }
}

如果需要对特定操作做独立错误处理,可以封装一个辅助函数来避免臃肿的 try/catch:

// 封装安全的异步调用
async function safeAsync(promise) {
  try {
    const data = await promise;
    return [data, null];
  } catch (err) {
    return [null, err];
  }
}

// 使用
const [user, userErr] = await safeAsync(fetchUser(1));
if (userErr) {
  handleUserError(userErr);
  return;
}
const [orders, ordersErr] = await safeAsync(fetchOrders(user.id));
if (ordersErr) {
  handleOrdersError(ordersErr);
  return;
}

并行执行

一个常见误区是把所有 await 都写成顺序执行。当多个异步操作之间互不依赖时,应该并行执行:

// 错误:顺序执行,总耗时 = 2s + 3s = 5s
async function loadSlow() {
  const users = await fetchUsers();     // 2 秒
  const products = await fetchProducts(); // 3 秒
  return { users, products };
}

// 正确:并行执行,总耗时 = max(2s, 3s) = 3s
async function loadFast() {
  const [users, products] = await Promise.all([
    fetchUsers(),
    fetchProducts()
  ]);
  return { users, products };
}

规律很简单:如果后面的操作依赖前面的结果,就用顺序 await;如果不依赖,就用 Promise.all 并行。

循环中的异步

在循环中使用 async/await 需要格外小心。for...of 配合 await 是顺序执行的,而 forEach 中的 await 不会等待前一次迭代完成:

// 顺序处理,每次等上一个完成
async function processSequentially(items) {
  for (const item of items) {
    await processItem(item);
  }
}

// 并行处理,所有同时开始
async function processInParallel(items) {
  await Promise.all(items.map(item => processItem(item)));
}

// 控制并发数量(实用模式)
async function processWithConcurrency(items, limit = 3) {
  const results = [];
  for (let i = 0; i < items.length; i += limit) {
    const batch = items.slice(i, i + limit);
    const batchResults = await Promise.all(
      batch.map(item => processItem(item))
    );
    results.push(...batchResults);
  }
  return results;
}

总结

Promise 和 async/await 不是互斥的,而是层层递进的关系。Promise 是异步编程的基础设施,async/await 是建立在 Promise 之上的语法糖。理解 Promise 的工作原理,才能正确地使用 async/await,避免陷入常见陷阱。在实际项目中,两者往往混合使用:简单场景用 async/await,并行控制用 Promise.all 等静态方法。

掌握这些知识后,你会发现自己写出的异步代码既简洁又健壮,再也不用面对层层嵌套的回调函数而头疼了。