异步编程进化论:从 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 个关键特性
- 状态不可逆:一旦从 pending(等待)变为 fulfilled 或 rejected,状态就固定不变。
- 链式调用本质:每个.then () 返回新的 Promise,可继续链式调用,解决回调嵌套问题。
- 错误冒泡:一个.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 静态方法,效率最高;
- 错误处理和边界情况(如取消、限流)是区分 “初级” 和 “高级” 开发者的关键。
异步编程没有银弹,最好的方案永远是 “适合当前场景” 的方案。希望这篇指南能帮你少踩坑、多提效~