在上一节中我们提到,回调函数虽然能解决异步问题,但多层嵌套会导致“回调地狱”——代码向右无限缩进、难以维护、错误处理分散。Promise 的出现正是为了将异步代码从嵌套的回调中解救出来,以一种更线性、更可控的方式组织异步操作。
12.2.1 Promise 是什么
Promise 是一个代表了某个异步操作最终结果的对象。你可以把它想象成一个“承诺”:它当前可能还没有结果,但在未来某个时刻会给出一个确定的答复——要么成功,要么失败。
// 创建一个 Promise,模拟一个异步操作
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve('数据获取成功');
} else {
reject('请求失败');
}
}, 1000);
});
Promise 拥有三种互斥的状态:
- pending(进行中):初始状态,操作尚未完成。
- fulfilled(已成功):操作成功完成,通过
resolve传出结果值。 - rejected(已失败):操作失败,通过
reject传出失败原因。
最核心的规则:状态一旦从 pending 变为 fulfilled 或 rejected,就不可再变。这种不可逆性保证了异步操作的终点是确定、纯粹的,也杜绝了回调中可能出现的“多次调用 resolve / reject”问题。
12.2.2 then、catch、finally
Promise 通过 then、catch 和 finally 方法提供了统一的回调注册方式。
then
then 方法接收两个可选的回调参数:第一个处理成功,第二个处理失败。
promise.then(
(value) => { console.log('成功:', value); },
(reason) => { console.log('失败:', reason); }
);
更重要的是,then 总是返回一个新的 Promise,这使链式调用成为可能:
doSomething()
.then(result => processResult(result))
.then(processed => saveResult(processed))
.then(() => console.log('全部完成'))
.catch(error => console.error('途中出错:', error));
链式调用彻底消除了多层回调的嵌套问题,让异步流程读起来像是同步顺序执行。
catch
catch(onRejected) 是 then(null, onRejected) 的语法糖,用来统一捕获链上前端任何环节抛出的错误。Promise 的错误会沿着链一直向后传递,直到遇到第一个 catch。
fetchUser()
.then(parseData)
.then(saveData)
.catch(error => {
// fetchUser、parseData、saveData 中任何一个环节出错都会落到这里
console.error('操作失败:', error);
});
finally
finally(callback) 无论 Promise 最终状态是 fulfilled 还是 rejected,都会执行。常用于清理资源、关闭 loading 动画、隐藏加载提示等收尾工作。它不像 then 或 catch 那样接收数据,也不改变链上传递的结果值。
showLoading();
fetchData()
.then(data => render(data))
.catch(error => showError(error))
.finally(() => hideLoading());
12.2.3 Promise 静态方法
ES6 提供的静态方法极大地丰富了并发场景下的控制能力。
Promise.resolve / Promise.reject
用来快速创建已敲定状态的 Promise,常用于将普通值包装成 Promise,或直接返回一个拒绝的 Promise。
Promise.resolve(42).then(v => console.log(v)); // 42
Promise.reject('fail').catch(e => console.log(e)); // 'fail'
Promise.all
接收一个可迭代对象(通常是数组),等所有 Promise 都成功,才返回包含所有结果数组的 Promise;只要有任何一个失败,立即返回失败原因,其余未完成的结果会被忽略。适合“全部任务都必须成功”的场景,例如同时请求多个接口的数据。
Promise.all([fetchUser(), fetchOrders(), fetchReports()])
.then(([user, orders, reports]) => {
// 全部成功,使用数据
})
.catch(error => {
// 任何一个失败都会进到这里
});
Promise.race
竞态方法,哪个 Promise 先改变状态(无论成功还是失败),就返回那个结果。常用于超时控制。
const timeout = new Promise((_, reject) => {
setTimeout(() => reject('请求超时'), 5000);
});
Promise.race([fetchData(), timeout])
.then(data => console.log('成功', data))
.catch(err => console.log('失败或超时', err));
Promise.allSettled
与 all 不同,allSettled 会等待所有 Promise 完成,无论成功还是失败,最后返回一个数组,每个元素包含 status 和 value 或 reason。适合需要获知所有操作结果、部分失败也不影响整体判断的场景。
Promise.allSettled([fetchUser(), fetchOrders(), fetchReports()])
.then(results => {
results.forEach(r => {
if (r.status === 'fulfilled') console.log('成功', r.value);
else console.log('失败', r.reason);
});
});
Promise.any
接收一个 Promise 数组,只要其中任何一个成功,就返回那个成功的值;如果所有 Promise 都失败,才拒绝。它相当于 all 的“宽松”兄弟,常用于多源竞速请求同一资源(从多个 CDN 获取同一个文件)。
Promise.any([fetchFromCDN1(), fetchFromCDN2(), fetchFromCDN3()])
.then(data => useData(data))
.catch(() => console.error('所有源都失败'));
12.2.4 实用技巧与常见误区
- 始终要返回结果:在链式调用的回调中,如果要让下一步拿到处理后的数据,必须用
return返回。忘记return会导致后续then收到undefined。 - 避免冗余嵌套:不要在
then里再去单独创建 Promise 然后再then,直接用链式调用延伸下去。 - 错误捕获位置:将
catch放在链的末端,可以统一处理上游错误;如果需要在中间处理错误并恢复流程,可以在合适位置插入catch并提供降级数据。 - 并发数量控制:当需要同时执行大量异步操作(如批量上传文件)时,应使用队列或限制并发数的方案,而不是一股脑全部放进
Promise.all,以免耗尽网络连接或内存。这部分将在第 12.4 节“异步并发控制”中详细展开。
Promise 改变了 JavaScript 异步编程的面貌,使异步流程从“嵌套回调”演进为“链式表达”。而下一节要学习的 async / await 进一步将这个链式表达变成了几乎是“同步写法”的优雅形态。理解 Promise 的原理,才能真正用好 async / await。