从 Promise 到 Async/Await 的异步编程优雅进化
在稀土掘金的技术社区中,异步编程一直是高频讨论话题。本文将带你从 Promise 的链式调用,一路进阶到 Async/Await 的同步化写法,并分享一些实战中的最佳实践。
1. 异步编程的痛点:回调地狱
在早期的 JavaScript 中,异步操作依赖回调函数。多个异步任务串联时,代码会迅速陷入"回调地狱"(Callback Hell):
javascript
getUser((user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
console.log(comments);
});
});
});
这种嵌套结构不仅可读性极差,而且难以维护和捕获错误。
2. Promise 的诞生:链式调用的曙光
Promise 是 ES6 引入的异步解决方案,它代表一个尚未完成但预期将来会完成的操作。Promise 解决了回调地狱的核心问题------通过 .then() 实现链式调用,通过 .catch() 统一捕获错误。
2.1 基本用法
javascript
const fetchUser = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: 1, name: '掘金小王子' });
}, 1000);
});
};
fetchUser()
.then(user => {
console.log(user);
return fetchPosts(user.id);
})
.then(posts => {
console.log(posts);
return fetchComments(posts[0].id);
})
.then(comments => {
console.log(comments);
})
.catch(err => {
console.error('出错了:', err);
});
2.2 静态方法
| 方法 | 作用 |
|---|---|
Promise.all() |
并行执行多个 Promise,所有成功才 resolve |
Promise.race() |
任意一个 Promise 完成即返回 |
Promise.allSettled() |
等待所有 Promise 结束(无论成功/失败) |
Promise.any() |
任意一个成功即返回,全部失败才 reject |
3. Async/Await:让异步代码"同步化"
尽管 Promise 已经改善了可读性,但链式调用在复杂场景下依然显得冗长。ES2017 推出了 async/await 语法糖,它让异步代码看起来像同步代码,极大提升了可维护性。
3.1 核心语法
javascript
async function getData() {
try {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
console.log(comments);
} catch (error) {
console.error('捕获异常:', error);
}
}
3.2 关键特性
- async 函数:始终返回一个 Promise。
- await 关键字:暂停函数执行,等待 Promise 落定。
- 错误处理 :使用
try/catch代替.catch()。
3.3 并行优化
await 会阻塞后续代码执行,如果多个异步任务互不依赖,应使用 Promise.all 并行执行:
javascript
// ❌ 串行执行(耗时累加)
const a = await fetchA();
const b = await fetchB();
// ✅ 并行执行(耗时取最大)
const [a, b] = await Promise.all([fetchA(), fetchB()]);
4. 实战场景:稀土掘金 API 调用示例
假设我们需要从掘金开放 API 获取用户信息和文章列表,并渲染到页面。
4.1 使用 Promise 链
javascript
function fetchJuejinUser(uid) {
return fetch(`https://api.juejin.cn/user/${uid}`)
.then(res => res.json())
.then(data => data.data)
.catch(err => console.error('用户信息请求失败', err));
}
function fetchJuejinArticles(uid) {
return fetch(`https://api.juejin.cn/articles?uid=${uid}`)
.then(res => res.json())
.then(data => data.list)
.catch(err => console.error('文章列表请求失败', err));
}
// 组合调用
fetchJuejinUser('123456')
.then(user => {
renderUser(user);
return fetchJuejinArticles(user.uid);
})
.then(articles => renderArticles(articles))
.catch(err => console.error('整体流程出错', err));
4.2 使用 Async/Await(推荐)
javascript
async function renderJuejinProfile(uid) {
try {
const user = await fetchJuejinUser(uid);
renderUser(user);
const articles = await fetchJuejinArticles(uid);
renderArticles(articles);
} catch (error) {
console.error('渲染失败:', error);
showErrorPage(error);
}
}
// 并行请求优化(用户信息和文章列表可同时进行)
async function renderJuejinProfileOptimized(uid) {
try {
const [user, articles] = await Promise.all([
fetchJuejinUser(uid),
fetchJuejinArticles(uid)
]);
renderUser(user);
renderArticles(articles);
} catch (error) {
console.error('并行请求失败:', error);
}
}
5. 错误处理最佳实践
5.1 不要忘记捕获异常
javascript
// ❌ 未捕获异常会导致程序崩溃
async function risky() {
const data = await fetchData(); // 可能 reject
return data;
}
// ✅ 使用 try/catch 包裹
async function safe() {
try {
const data = await fetchData();
return data;
} catch (err) {
logger.error(err);
return defaultData;
}
}
5.2 全局异常兜底
javascript
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 异常:', event.reason);
// 可上报到监控系统
});
6. 常见陷阱与进阶技巧
| 陷阱 | 解决方案 |
|---|---|
在循环中使用 await 导致串行执行 |
使用 Promise.all + map 并行 |
忘记 await 关键字 |
使用 ESLint 规则 no-await-in-loop |
在顶层作用域使用 await(ES Module 支持) |
使用立即执行函数 (async () => {})() |
| 未处理的 reject | 始终添加 .catch() 或 try/catch |
6.1 循环中的正确用法
javascript
// ❌ 串行执行(慢)
for (const id of ids) {
const data = await fetchItem(id);
process(data);
}
// ✅ 并行执行(快)
const promises = ids.map(id => fetchItem(id));
const results = await Promise.all(promises);
results.forEach(process);
7. 在稀土掘金社区的讨论热度
掘金上关于 async/await 的讨论文章已超过 500+ 篇,其中最受欢迎的议题包括:
- "Async/Await 真的比 Promise 更优吗?"
- "如何优雅地管理多个异步请求?"
- "在 React/Vue 项目中如何结合 Hooks 使用 Async?"
可见,异步编程依然是前端工程师进阶的核心技能之一。
8. 总结
| 方案 | 优点 | 缺点 |
|---|---|---|
| 回调 | 简单直接 | 回调地狱,难以维护 |
| Promise | 链式调用,统一错误处理 | 长链可读性下降 |
| Async/Await | 同步式写法,易于理解和调试 | 需注意并行优化和异常捕获 |
在稀土掘金社区,我们鼓励开发者优先使用 async/await 编写新代码,同时配合 Promise.all 优化并发性能。只有深入理解其底层原理(微任务队列、事件循环),才能写出既优雅又健壮的异步代码。
本文首发于 稀土掘金 技术社区,欢迎在评论区分享你的异步编程经验。