【JS 异步】告别回调地狱:Async/Await 和 Promise 的优雅实践与错误处理
【JS 异步】告别回调地狱:Async/Await 和 Promise 的优雅实践与错误处理
所属专栏: 《前端小技巧集合:让你的代码更优雅高效
上一篇: 【JS 数组】数组操作的“瑞士军刀”:精通 Array.reduce() 的骚操作
作者: 码力无边
✨ 引言:那座名为“回调地狱”的金字塔,我们曾亲手搭建
嘿,各位在代码世界里追求光与热的道友们,我是码力无边!
在我们的前端江湖中,如果说 DOM 操作是“外家功夫”,数据处理是“内功心法”,那么异步编程,就是那门决定你能否“御剑飞行”的“轻功”。因为在 Web 世界,几乎所有有价值的操作都是异步的:
- 向服务器请求数据(Ajax/Fetch)
- 读取本地文件
- 设置一个定时器 (
setTimeout) - 等待用户点击一个按钮
这些操作都不会立即返回结果。JavaScript 作为一门单线程语言,为了不被这些耗时的任务阻塞主线程(否则页面就会卡死),它采用了“稍后处理”的异步模型。而在远古时代,我们实现这种“稍后处理”的唯一方式,就是回调函数 (Callback)。
让我们一起瞻仰一下那座由我们亲手搭建,又让我们备受折磨的“史前遗迹”——回调地狱 (Callback Hell),又称“毁灭金字塔 (Pyramid of Doom)”:
// 史前遗迹,请勿模仿
ajax('api/user/1', function(user) {
console.log('获取到用户:', user);
ajax(`api/posts?userId=${user.id}`, function(posts) {
console.log('获取到帖子:', posts);
ajax(`api/comments?postId=${posts[0].id}`, function(comments) {
console.log('获取到评论:', comments);
// 如果还有下一步...天啊...
ajax(`api/replies?commentId=${comments[0].id}`, function(replies) {
console.log('获取到回复:', replies);
// 金字塔已经高耸入云...
}, function(error) {
// 每一层都要处理错误...
});
}, function(error) {
// ...
});
}, function(error) {
// ...
});
}, function(error) {
// ...
});
这种代码,就像一个向右无限延伸的俄罗斯套娃。它的问题显而易见:
- 可读性极差:代码逻辑不是从上到下,而是像贪吃蛇一样扭曲。
- 难以维护:想在中间加一步?或者修改某一步的逻辑?祝你好运。
- 错误处理复杂:每一层嵌套都需要单独处理错误,很容易遗漏。
为了推翻这座压迫我们多年的“金字塔”,JavaScript 社区的先贤们进行了不懈的斗争,最终为我们带来了两件划时代的法宝:Promise 和 async/await。
今天,码力无边就将带你走过这条从“地狱”到“天堂”的救赎之路,让你彻底掌握现代 JavaScript 中最优雅、最强大的异步编程范式。
一、Promise:从“回调”到“承诺”的革命
Promise 的出现,是异步编程思想的一次伟大飞跃。它把“传递一个函数进去等待执行”的模式,变成了“给你一个承诺对象,你拿着它等结果”的模式。
一个 Promise 对象,就像一张“彩票”。你买下它的时候,它处于 pending (进行中) 状态。未来,它可能会中奖,变成 fulfilled (已成功) 状态,并给你奖金(结果值);也可能没中奖,变成 rejected (已失败) 状态,并告诉你原因(错误信息)。
1.1 用 .then() 链式调用,拆解金字塔
Promise 最核心的变革,就是引入了 .then() 方法。.then() 方法可以接收两个函数作为参数,一个用于处理成功状态,一个用于处理失败状态。更重要的是,.then() 方法会返回一个新的 Promise 对象,这使得我们可以进行链式调用!
让我们用 Promise 来重构上面的“地狱”代码:
// 假设 ajax 函数现在返回一个 Promise
ajax('api/user/1')
.then(user => {
console.log('获取到用户:', user);
// 返回一个新的 Promise
return ajax(`api/posts?userId=${user.id}`);
})
.then(posts => {
console.log('获取到帖子:', posts);
// 返回又一个新的 Promise
return ajax(`api/comments?postId=${posts[0].id}`);
})
.then(comments => {
console.log('获取到评论:', comments);
return ajax(`api/replies?commentId=${comments[0].id}`);
})
.then(replies => {
console.log('获取到回复:', replies);
})
.catch(error => {
// 革命性的改变:用一个 .catch() 捕获链条上任何一个环节的错误!
console.error('发生错误:', error);
});
看到了吗?金字塔被夷为平地!代码变成了从上到下的线性结构,逻辑清晰无比。
- 线性流程:每一步操作都清晰地写在一个
.then()中。 - 统一错误处理:链式调用中任何一个 Promise 变成
rejected,都会被最后的.catch()捕获。告别了层层嵌套的错误处理。
1.2 Promise 的“静态方法”:Promise.all 和 Promise.race
Promise 还提供了一些强大的工具函数:
-
Promise.all(iterable): 并行执行,等待所有。- 场景:你需要同时请求用户基本信息、用户的好友列表和用户的相册,三者没有依赖关系,但你希望等它们全部成功后,再渲染页面。
- 用法:它接收一个 Promise 数组,返回一个新的 Promise。只有当数组中所有的 Promise 都成功时,它才会成功,并且结果是一个包含所有 Promise 结果的数组。如果其中任何一个失败了,它就会立刻失败。
Promise.all([ ajax('api/userInfo'), ajax('api/friendList'), ajax('api/album') ]).then(([userInfo, friendList, album]) => { // 在这里,三个请求都已成功完成 renderPage(userInfo, friendList, album); }).catch(error => { // 只要有一个请求失败,就会进入这里 showErrorPage(error); }); -
Promise.race(iterable): 并行执行,谁快用谁。- 场景:你向两个不同的 CDN 节点请求同一个资源,哪个先返回就用哪个。或者,给一个请求设置超时:让你的请求和
setTimeout返回的 Promise 赛跑。 - 用法:它也接收一个 Promise 数组,但只要其中任何一个 Promise 率先改变状态(无论是成功还是失败),它就会立即采用那个 Promise 的状态和结果。
function requestWithTimeout(url, timeout) { let timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('请求超时!')), timeout); }); return Promise.race([ fetch(url), timeoutPromise ]); } requestWithTimeout('api/slow-resource', 3000) .then(response => console.log('请求成功:', response)) .catch(error => console.error(error.message)); // 可能是网络错误,也可能是“请求超时!” - 场景:你向两个不同的 CDN 节点请求同一个资源,哪个先返回就用哪个。或者,给一个请求设置超时:让你的请求和
Promise 已经非常强大了,但它仍然需要 .then() 的回调函数语法。人类的大脑,终究还是更习惯同步的、阻塞式的代码写法。于是,终极形态的“异步救世主”登场了。
二、async/await:用写同步代码的方式,来写异步
async/await 是 ES2017 (ES8) 引入的,它并不是一个新东西,而是建立在 Promise 之上的语法糖 (Syntactic Sugar)。它的目标只有一个:让异步代码看起来、写起来都像同步代码。
两个关键词:
async: 用来修饰一个函数,表明这个函数是一个异步函数。任何async函数的返回值,都会被自动包装成一个 Promise。await: 只能用在async函数内部。它后面通常跟着一个 Promise。它的作用是**“暂停”当前async函数的执行,等待后面的 Promise 状态变为fulfilled,然后直接返回 Promise 的结果值**。如果 Promise 失败了,它会抛出 (throw) 错误。
2.1 终极进化:最“人类友好”的异步代码
让我们用 async/await 来重写我们最初的那个例子,你将见证代码的可读性如何达到巅峰:
async function fetchAllData() {
try {
// 代码像同步一样,从上到下执行
const user = await ajax('api/user/1');
console.log('获取到用户:', user);
const posts = await ajax(`api/posts?userId=${user.id}`);
console.log('获取到帖子:', posts);
const comments = await ajax(`api/comments?postId=${posts[0].id}`);
console.log('获取到评论:', comments);
const replies = await ajax(`api/replies?commentId=${comments[0].id}`);
console.log('获取到回复:', replies);
return replies; // async 函数的返回值
} catch (error) {
// 同样革命性的改变:用标准的 try...catch 来捕获所有 await 的错误!
console.error('发生错误:', error);
}
}
fetchAllData().then(result => {
console.log('所有数据获取完毕:', result);
});
震撼吗?
- 完全同步的写法:没有
.then,没有回调,代码的执行顺序和你的阅读顺序完全一致。 - 标准的错误处理:
try...catch是我们再熟悉不过的同步代码错误处理机制,现在它完美地适用于异步流程。任何一个await的 Promise 失败,都会被catch块捕获。 - 优雅的返回值:
async函数的返回值就是一个 Promise,你可以继续在外部用.then()来处理最终的结果。
async/await 同样能和 Promise.all 等工具完美结合:
async function fetchParallelData() {
try {
const [userInfo, friendList, album] = await Promise.all([
ajax('api/userInfo'),
ajax('api/friendList'),
ajax('api/album')
]);
renderPage(userInfo, friendList, album);
} catch (error) {
showErrorPage(error);
}
}
三、现代异步编程最佳实践
-
优先使用
async/await:在任何可以使用它的地方(现代浏览器、Node.js、或经过 Babel 等工具编译的环境),async/await都应该是你的首选。它的可读性和可维护性是无与伦比的。 -
不要忘记
Promise.all:在使用async/await时,要警惕一种反模式——串行执行本可以并行的任务。// 反模式:不必要的串行等待 async function getTwoThings() { const thing1 = await fetchThing1(); // 等待 thing1 const thing2 = await fetchThing2(); // thing1 好了才开始请求 thing2 return [thing1, thing2]; } // 正确模式:并行执行 async function getTwoThingsInParallel() { const [thing1, thing2] = await Promise.all([ fetchThing1(), fetchThing2() ]); return [thing1, thing2]; } -
顶层
await:最新的 JavaScript (ES2022) 已经支持在模块的顶层使用await,无需包裹在async函数中。这在一些初始化脚本中非常有用。
写在最后:从驯服异步,到驾驭异步
从回调地狱的混乱,到 Promise 链的秩序,再到 async/await 的优雅,JavaScript 的异步编程演进史,就是一部不断追求“人性化”和“可读性”的奋斗史。
掌握 async/await 和 Promise,你就不再是那个被异步任务牵着鼻子走的“回调奴隶”,而是一个能够从容地编排、组织、驾驭复杂异步流程的“时间管理者”。你的代码将不再是难以理解的“面条”,而是结构清晰、逻辑顺畅的“诗篇”。
所以,道友们,请彻底告别回调地狱吧。在你的下一个项目中,大胆地拥抱 async/await,用最现代、最优雅的方式,去驯服时间,驾驭异步!
专栏预告与互动:
我们已经掌握了现代 JS 的异步核心。但代码写得再优雅,也得简洁。ES6 引入了许多强大的“语法糖”,它们能让你用更少的代码,做更多的事。
下一篇,我们将深入 ES6+ 的代码整洁之道,探索解构赋值和展开语法的 5 个神仙用法,让你的代码瞬间“瘦身”,可读性翻倍!
码力无边的异步心法,你 Get 了吗?点赞、收藏、关注,用你的三连,为我的下一次“瞬移”积蓄能量!
今日思考题:
forEach循环是同步的,如果我在forEach的回调里使用await,会发生什么?它会按顺序等待每一个await完成吗?为什么?这是一个经典的async/await陷阱,把你的分析写在评论区,我们一起探讨!
更多推荐


所有评论(0)