如何优雅处理多个异步操作?
在 JavaScript 开发中,经常会遇到需要处理多个异步操作的需要,为避免陷入回调地狱或逻辑混乱,总结了处理多异步任务的方法,包含 Promise
、async/await
及常见陷阱解决方案。
1、传统异步问题:回调地狱
// 嵌套地狱示例
fetchUser(user => {
fetchOrders(user.id, orders => {
orders.forEach(order => {
fetchDetails(order.id, details => {
// ... 逻辑层层嵌套
});
});
});
});
这种代码难以维护,而现代 JavaScript 提供了更优雅的解决方案。
2. Promise 链式调用
2.1. 基本链式操作
fetchUser()
.then(user => fetchOrders(user.id))
.then(orders => Promise.all(orders.map(order => fetchDetails(order.id))))
.then(allDetails => console.log("所有订单详情:", allDetails))
.catch(error => console.error("链式调用失败:", error));
2.2. 关键特性
- 错误冒泡:链中任一环节的错误会直接跳到最近的
catch()
- 扁平化结构:避免嵌套,通过
return
传递结果
3. 并行处理
3.1. Promise.all
全成功才成功。
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
// 所有请求完成后统一处理
console.log("用户:", user);
console.log("文章:", posts);
console.log("评论:", comments);
适用场景:
- 所有任务相互独立且必须全部完成
- 需要聚合多个接口数据后再渲染页面
3.2. Promise.allSettled
无论成功与否都等待。
const results = await Promise.allSettled([
fetchUser(),
fetchPosts().then(() => { throw new Error("模拟失败") }),
fetchComments()
]);
results.forEach(result => {
if (result.status === "fulfilled") {
console.log("成功结果:", result.value);
} else {
console.error("失败原因:", result.reason);
}
});
适用场景:
- 需要处理部分成功/失败的情况(如表单多步骤提交)
- 监控多个独立任务的最终状态
4. 竞速处理
4.1. Promise.race
以第一个完成为准。
// 超时控制示例
const fetchWithTimeout = Promise.race([
fetchData(),
new Promise((_, reject) => setTimeout(() => reject("请求超时"), 3000))
]);
fetchWithTimeout
.then(data => console.log("成功获取数据"))
.catch(error => console.error(error)); // 3秒内未完成则触发超时
4.2. Promise.any
以第一个成功为准(ES2021)。
// 多 CDN 备用请求
const cdnProviders = [
fetchFromCDN1(),
fetchFromCDN2(),
fetchFromCDN3()
];
Promise.any(cdnProviders)
.then(resource => console.log("成功加载资源"))
.catch(error => console.error("所有 CDN 均失败"));
5. 错误处理策略
5.1. 集中式错误处理
async function handleData() {
try {
const data = await fetchData();
const processed = await processData(data);
await saveData(processed);
} catch (error) {
// 统一处理所有错误
handleError(error);
}
}
5.2. 选择性错误捕获
// 捕获特定错误
try {
await riskyOperation();
} catch (error) {
if (error.code === 404) {
handleNotFound();
} else if (error.code === 401) {
redirectToLogin();
} else {
throw error; // 重新抛出未知错误
}
}
6. 进阶技巧:异步控制流
6.1. 限制并发数
// 并发控制函数(最多同时3个请求)
async function processInBatches(tasks, concurrency) {
const results = [];
for (let i = 0; i < tasks.length; i += concurrency) {
const batch = tasks.slice(i, i + concurrency);
results.push(...await Promise.all(batch.map(task => task())));
}
return results;
}
// 使用示例
const tasks = [...Array(10).keys()].map(i => () => fetchData(i));
processInBatches(tasks, 3);
6.2. 串行执行
async function processSerially(tasks) {
const results = [];
for (const task of tasks) {
results.push(await task());
}
return results;
}
// 使用示例
processSerially([
() => step1(),
() => step2(),
() => step3()
]);
7. 常见陷阱与解决方案
7.1. 遗漏错误处理
// ❌ 错误写法:未捕获错误
Promise.all([fetch1(), fetch2()])
.then(() => console.log("成功"));
// ✅ 正确写法:始终添加 catch
Promise.all([...])
.then(...)
.catch(error => {
// 处理错误或记录日志
});
7.2. 忽略清理操作
// 使用 AbortController 中止请求(Fetch API)
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
fetch(url, { signal: controller.signal })
.catch(err => {
if (err.name === "AbortError") {
console.log("请求已中止");
}
});
8. 代码结构优化建议
8.1. 分离业务逻辑与异步逻辑
// 业务逻辑层
async function loadData() {
const data = await fetchData();
return processData(data);
}
// UI 层
async function renderPage() {
try {
const data = await loadData();
updateUI(data);
} catch (error) {
showError(error);
}
}
8.2. 使用 async/await 简化代码
// ❌ 嵌套写法
fetchUser(user => {
fetchOrders(user.id, orders => {
// ...
});
});
// ✅ 扁平化写法
async function loadUserData() {
const user = await fetchUser();
const orders = await fetchOrders(user.id);
// ...
}
9. 性能优化建议
- 预加载数据:在空闲时间预取可能用到的数据
- 缓存策略:对重复请求进行内存或本地存储缓存
- 请求合并:将多个小请求合并为一个批量请求
- 优先级调度:对关键任务设置更高的优先级
10. 总结
掌握多异步操作处理的关键在于:
- 根据场景选择合适的 API(
all
/race
/any
) - 建立清晰的错误处理机制
- 保持代码结构的简洁性