异步编程进化论:从 Promise 到 async/await 的 10 个实战技

55 阅读8分钟

异步编程进化论:从 Promise 到 async/await 的 10 个实战技巧

每个前端开发者都绕不开异步编程的 “修炼”—— 从回调地狱的 “层层嵌套”,到 Promise 的 “链式救赎”,再到 async/await 的 “同步优雅”,JavaScript 的异步方案迭代,本质上是一场 “让代码更易读、让开发者少掉发” 的革命。

作为踩过无数坑的掘金创作者,我整理了这份从 Promise 到 async/await 的全场景实战指南:既有基础逻辑拆解,也有进阶技巧(如并发控制、异步取消),更有 90% 开发者踩过的避坑点,代码可直接复制到项目中使用,面试高频考点也一并覆盖~

一、基础回顾:从 “回调地狱” 到 “异步契约”

在 Promise 出现之前,我们靠回调函数处理异步,代码像 “俄罗斯套娃” 一样层层嵌套,这就是臭名昭著的回调地狱

javascript

// ❌ 回调地狱:嵌套3层以上,可读性和可维护性直接崩盘
getUser(userId, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetails(orders[0].id, function(details) {
      getShippingInfo(details.shippingId, function(shipping) {
        console.log('最终数据:', shipping);
      }, handleError);
    }, handleError);
  }, handleError);
}, handleError);

Promise:异步操作的 “契约书”

Promise 的核心是为异步操作提供状态管理链式调用,它就像一份 “契约”:承诺异步操作完成后,要么返回结果(fulfilled),要么返回错误(rejected)。

javascript

// ✅ Promise链式调用:打破嵌套,流程清晰
getUser(userId)
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetails(orders[0].id))
  .then(details => getShippingInfo(details.shippingId))
  .then(shipping => console.log('最终数据:', shipping))
  .catch(error => console.error('出错了:', error));
Promise 的 3 个关键特性
  1. 状态不可逆:一旦从 pending(等待)变为 fulfilled 或 rejected,状态就固定不变。
  2. 链式调用本质:每个.then () 返回新的 Promise,可继续链式调用,解决回调嵌套问题。
  3. 错误冒泡:一个.catch () 可捕获前面所有.then () 的错误,无需重复写错误处理。

async/await:Promise 的 “语法糖蛋糕”

ES7 推出的 async/await,让异步代码看起来像同步代码一样直观,本质是 Promise 的语法糖,但用起来更 “甜”。

javascript

// ✅ async/await:同步写法,异步执行
async function getFullShippingInfo(userId) {
  try {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    const details = await getOrderDetails(orders[0].id);
    const shipping = await getShippingInfo(details.shippingId);
    return shipping;
  } catch (error) {
    console.error('出错了:', error);
    throw error; // 可重新抛出,让调用者处理
  }
}
async/await 的 2 个核心规则
  • 函数必须加async关键字,否则不能使用await
  • await只能在 async 函数内使用,会暂停函数执行,等待 Promise 决议。

二、进阶技巧:从 “能用” 到 “好用” 的关键

掌握基础只是入门,这些实战技巧能让你在项目中真正发挥 Promise 和 async/await 的威力。

1. 并发控制:用 Promise 静态方法提升效率

异步操作不是 “等一个完再等下一个”,合理利用并发能大幅提升性能,这是面试高频考点!

方法作用适用场景
Promise.all()等待所有 Promise 成功,返回结果数组多个独立异步操作,需全部完成才继续
Promise.allSettled()等待所有 Promise 决议(成功 / 失败),返回状态数组需获取所有操作结果,允许部分失败
Promise.race()第一个决议的 Promise 决定结果超时控制、竞争条件(如同时请求多个接口取最快响应)
Promise.any()第一个成功的 Promise 决定结果,全部失败才 reject多个备选接口,取第一个成功响应

javascript

// 🔥 实战:批量上传文件,需所有文件上传成功才提交(Promise.all())
async function batchUpload(files) {
  try {
    // 生成所有上传Promise
    const uploadPromises = files.map(file => uploadToCloud(file));
    // 等待所有上传完成,返回结果数组
    const fileUrls = await Promise.all(uploadPromises);
    console.log('所有文件上传成功:', fileUrls);
    return fileUrls;
  } catch (error) {
    console.error('至少一个文件上传失败:', error);
  }
}

// 🔥 实战:获取用户信息和商品列表,允许其中一个失败(Promise.allSettled())
async function fetchUserAndProducts() {
  const [userRes, productsRes] = await Promise.allSettled([
    fetchUser(),
    fetchProducts()
  ]);
  
  // 分别处理成功和失败
  const user = userRes.status === 'fulfilled' ? userRes.value : null;
  const products = productsRes.status === 'fulfilled' ? productsRes.value : [];
  
  return { user, products };
}

2. 异步循环:避免 “串行陷阱”

在循环中使用await很容易踩坑,默认是串行执行,效率极低!

javascript

// ❌ 错误:串行执行,10个请求需等10倍时间
async function fetchItemsSerial(ids) {
  const results = [];
  for (const id of ids) {
    const item = await fetchItem(id); // 等上一个完成才执行下一个
    results.push(item);
  }
  return results;
}

// ✅ 正确:并行执行,所有请求同时发起
async function fetchItemsParallel(ids) {
  const promises = ids.map(id => fetchItem(id)); // 先创建所有Promise
  const results = await Promise.all(promises); // 同时等待所有结果
  return results;
}

// ✅ 进阶:有限并发(避免请求过多压垮服务器)
import pLimit from 'p-limit';

async function fetchItemsWithLimit(ids, limit = 5) {
  const limitFn = pLimit(limit); // 限制并发数为5
  const promises = ids.map(id => limitFn(() => fetchItem(id)));
  const results = await Promise.all(promises);
  return results;
}

3. 错误处理:从 “简单捕获” 到 “精准定位”

错误处理是异步编程的重中之重,不同场景需要不同的处理方式。

方式 1:全局捕获未处理的 Promise 错误

避免因遗漏.catch () 导致的静默失败:

javascript

// 捕获全局未处理的Promise拒绝
window.addEventListener('unhandledrejection', (event) => {
  console.error('未处理的Promise错误:', event.reason);
  event.preventDefault(); // 阻止浏览器默认行为
});
方式 2:用 Error cause 串联错误链路

异步调用层级多时,用cause保留原始错误,方便排查:

javascript

async function processOrder(orderId) {
  try {
    const order = await fetchOrder(orderId);
    const payment = await validatePayment(order);
    return payment;
  } catch (err) {
    // 包装错误,保留根因
    throw new Error(`订单处理失败(ID:${orderId})`, { cause: err });
  }
}

// 调用时可追溯根因
try {
  await processOrder('ORD123456');
} catch (e) {
  console.error('业务错误:', e.message);
  console.error('技术根因:', e.cause); // 原始错误
}
方式 3:用 “to 函数” 简化 try/catch 冗余

多次 await 时,重复的 try/catch 会让代码臃肿,用辅助函数优化:

javascript

// 辅助函数:将Promise转为[error, data]格式
function to(promise) {
  return promise.then(data => [null, data]).catch(error => [error, null]);
}

// 实战使用:无需嵌套try/catch
async function getUserProfile(userId) {
  const [userErr, user] = await to(fetchUser(userId));
  if (userErr) return { error: '用户获取失败' };
  
  const [profileErr, profile] = await to(fetchProfile(user.id));
  if (profileErr) return { error: '用户资料获取失败' };
  
  return { user, profile };
}

4. 异步取消:用 AbortController 中断操作

场景:用户切换路由时,中断当前未完成的接口请求,避免资源浪费或数据错乱。

javascript

// 🔥 实战:可取消的接口请求
async function fetchWithCancel(url, options = {}) {
  const controller = new AbortController();
  const signal = controller.signal;
  
  // 合并AbortSignal到请求选项
  const fetchOptions = { ...options, signal };
  
  // 发起请求
  const fetchPromise = fetch(url, fetchOptions);
  
  // 返回请求Promise和取消函数
  return {
    promise: fetchPromise.then(res => res.json()),
    cancel: () => controller.abort('用户主动取消请求')
  };
}

// 使用示例
const { promise: userPromise, cancel: cancelFetch } = fetchWithCancel('/api/user');

// 需取消时(如路由切换)
cancelFetch();

try {
  const user = await userPromise;
} catch (err) {
  if (err.name === 'AbortError') {
    console.log('请求已取消:', err.message);
  }
}

三、避坑指南:90% 开发者踩过的 5 个坑

1. 忘记处理 Promise 错误

javascript

// ❌ 危险:未处理的reject会导致全局错误
async function fetchData() {
  const data = await fetch('/api/data'); // 若失败,会抛出未捕获异常
}

✅ 修复:始终用catch()try/catch处理错误。

2. 混淆 Promise.all () 的 “快速失败” 特性

javascript

// ❌ 问题:只要一个Promise失败,整个Promise.all()就会立即失败
const [user, products] = await Promise.all([fetchUser(), fetchProducts()]);

✅ 修复:需全部结果时,用Promise.allSettled()替代。

3. 在非 async 函数中使用 await

javascript

// ❌ 错误:SyntaxError
function getDate() {
  const data = await fetch('/api/date');
}

✅ 修复:函数必须加async关键字,或用.then () 链式调用。

4. 认为 async 函数返回值是原始值

javascript

// ❌ 误解:async函数始终返回Promise
async function getNum() {
  return 100;
}
const num = getNum(); // num是Promise对象,不是100

✅ 修复:用await.then()获取值:const num = await getNum();

5. 循环中滥用 await 导致串行

前面已讲过,这里再强调:循环中直接用 await 会串行执行,需并行时用Promise.all()

四、面试高频:3 个核心考点解析

1. Promise 的状态变化机制

问题:Promise 的状态有哪几种?能否从 fulfilled 转为 rejected?答案

  • 三种状态:pending(等待)、fulfilled(成功)、rejected(失败)。
  • 状态不可逆:一旦从 pending 转为 fulfilled 或 rejected,就无法再改变。

2. async/await 的执行顺序

问题:下面代码的输出顺序是什么?

javascript

console.log('script start');
async function async1() {
  await async2();
  console.log('async1 end');
}
async function async2() {
  console.log('async2 end');
}
async1();
new Promise(resolve => {
  console.log('Promise');
  resolve();
}).then(() => console.log('promise1'));
console.log('script end');

答案

plaintext

script start → async2 end → Promise → script end → promise1 → async1 end

解析

  • await 会暂停 async 函数,将后续代码推入微任务队列。
  • 同步代码执行完后,先处理所有微任务,再处理宏任务。

3. Promise.all () 和 Promise.race () 的区别

答案

  • Promise.all ():等待所有 Promise 成功,返回结果数组;任一失败则立即返回失败原因。
  • Promise.race ():第一个决议(成功或失败)的 Promise 决定最终结果,其余结果会被忽略。

五、总结:异步编程的核心思维

从 Promise 到 async/await,异步编程的演进方向是 “降低认知负荷”—— 让代码结构更贴近人类的线性思维,同时保留异步的高效性。

掌握这些技巧后,你会发现:

  • 简单异步流程用async/await,代码最简洁;
  • 复杂并发场景用 Promise 静态方法,效率最高;
  • 错误处理和边界情况(如取消、限流)是区分 “初级” 和 “高级” 开发者的关键。

异步编程没有银弹,最好的方案永远是 “适合当前场景” 的方案。希望这篇指南能帮你少踩坑、多提效~