你有没有遇到过这样的烦恼:代码里层层嵌套的回调像"千层饼",越写越往右偏,查个错误要一层层扒开?别急,Promise 就是拯救你脱离"回调地狱"的那束光。今天,我们从头到尾把 Promise 的所有知识点掰开揉碎,让你一次彻彻底底搞懂它。
一、什么是 Promise?一个外卖订单的比喻
想象你在外卖 App 上点了一份麻辣烫。付完款后,App 不会立刻把餐送到你手里,而是给你一个「订单」。这个订单就是 Promise ------ 它现在手上没有餐,但它承诺在未来的某个时刻,把餐的状态告诉你。
在此过程中:
下单后,订单状态是 "待配送 " ------ 对应 Promise 的 pending(等待中)。
骑手送到后,状态变成 "已完成 " ------ 对应 fulfilled,你拿到餐。
如果店铺关门或者骑手摔了,订单变成 "已取消 " ------ 对应 rejected,平台告诉你失败的原因。
核心结论:Promise 是一个"异步操作的结果容器",帮你把"等待、成功、失败"这三种状态管得明明白白,不用再嵌套回掉了。
二、为什么需要 Promise?告别回调地狱
在 Promise 出现之前,异步任务只能靠回调函数串起来。比如:先获取用户信息,再获取他的帖子,再拿第一篇帖子的评论。用回调写法大概是这样的:
js
getUser(userId, function(user) {
getPosts(user.id, function(posts) {
getComments(posts.id, function(comments) {
console.log(comments);
// 还有下一步?继续缩进...
}, errorCallback);
}, errorCallback);
}, errorCallback);
代码一层层往右缩进,错误处理重复写,维护起来简直像走迷宫。这就是著名的"回调地狱"(Callback Hell),也叫"末日金字塔"。
而用了 Promise,同样的逻辑变成了链式调用:
getUser(userId)
.then(user => getPosts(user.id))
.then(posts => getComments(posts.id))
.then(comments => console.log(comments))
.catch(error => handleError(error));
代码垂直向下流动,缩进始终一层,错误处理集中到末尾。可读性提升了不止一个档次。在 GitHub 上排名前 1000 的 JavaScript 项目里,Promise 的使用率已超过 90%,回调模式基本退出历史舞台。
三、Promise 的三种状态:终身不可逆
Promise 的生命周期只有三种状态,而且一旦状态改变就永久固定,再也回不去:
| 状态 | 含义 | 触发条件 |
|---|---|---|
| pending | 进行中(初始) | Promise 刚创建 |
| fulfilled | 已成功 | 调用了 resolve() |
| rejected | 已失败 | 调用了 reject() 或抛出异常 |
关键规则:
从 pending 可以变成 fulfilled 或 rejected,但一旦变了就不能再变。
即使你事后才去添加回调,Promise 也会记住当初的结果并立刻执行回调。
例如:
js
const p = new Promise((resolve, reject) => {
resolve('第一次成功');
reject('第二次失败'); // 这行无效,状态已经凝固
});
p.then(console.log); // 只会输出 '第一次成功'
四、创建 Promise:一个执行器搞定一切
创建 Promise 的语法很简单,只有 new Promise(executor),其中 executor 是一个回调函数,接受两个参数:
resolve:异步成功时调用,把状态变成 fulfilled,并传出结果。
reject:异步失败时调用,把状态变成 rejected,并传出原因(建议传 Error 对象)。
js
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve('数据加载成功');
} else {
reject(new Error('网络超时'));
}
}, 1000);
});
注意:executor 是立即执行的,而 then/catch 里的回调们是微任务,会等到同步代码跑完才执行。
五、处理结果:.then、.catch、.finally 三剑客
Promise 实例提供三个核心方法来处理结果:
- .then(onFulfilled, onRejected)
第一个参数处理 fulfilled 状态,接收 resolve 传出的值。
第二个参数(可选)处理 rejected 状态,但实战中用 .catch 更清晰。
返回值:每次调用 .then 都会返回一个新的 Promise,所以可以一直链下去。
js
promise.then(
result => console.log('成功:', result),
error => console.log('失败:', error) // 不推荐,请用 catch
);
- .catch(onRejected)
专门捕获 rejected 状态,或者链中任何一步抛出的错误。
js
promise
.then(data => JSON.parse(data))
.catch(err => console.error('捕获到错误:', err));
// 如果 JSON.parse 出错也会进入 catch
- .finally(onFinally)
无论最终状态是成功还是失败,都会执行。通常用来做清理工作,比如关闭 loading 动画、释放资源。它拿不到结果值。
js
promise
.then(data => console.log(data))
.catch(err => console.error(err))
.finally(() => console.log('请求结束'));
六、链式调用:让异步流程"平"起来
链式调用的威力在于:每个 .then 的回调返回值决定了下一个 .then 的状态和值。
返回一个普通值(非 Promise),下一个 .then 立刻获得这个值。
返回一个 Promise,下一个 .then 会等待它完成,并"继承"其状态和值。
如果抛出了错误,直接跳到最近的 .catch。
看一个流畅的链式例子(基于"打开冰箱 - 拿可乐 - 喝可乐"的异步):
js
// 封装一个返回 Promise 的等待函数
function wait(ms, msg) {
return new Promise(resolve => setTimeout(() => resolve(msg), ms));
}
wait(1000, '第一步:打开冰箱')
.then(msg => {
console.log(msg);
return wait(1000, '第二步:拿出可乐'); // 返回 Promise
})
.then(msg => {
console.log(msg);
return wait(1000, '第三步:喝可乐');
})
.then(msg => {
console.log(msg);
// 这里返回一个普通值
return '喝完了,好爽!';
})
.then(console.log)
.catch(err => console.error('出错啦', err));
每一个 .then 都在等前一个步骤完成后才执行,代码从上到下一气呵成。
七、Promise 静态方法:批量处理异步任务
很多时候我们需要同时处理多个异步操作,Promise 提供了一套实用的静态方法。
1. Promise.resolve(value)
快速创建一个 fulfilled 状态的 Promise。
js
Promise.resolve(42).then(console.log); // 输出 42
// 如果传入的是一个 Promise,则直接返回该 Promise
2. Promise.reject(reason)
快速创建一个 rejected 状态的 Promise。
js
Promise.reject(new Error('爆炸')).catch(console.error);
3. Promise.all(iterable)*
等待所有 Promise 都成功,才返回一个包含所有结果的数组。如果有一个失败,整体立刻失败,进入 catch。
js
Promise.all([fetch('/api/user'), fetch('/api/posts')])
.then(([user, posts]) => { /* 所有请求都成功 */ })
.catch(err => { /* 只需有一个失败就到这里 */ });
4. Promise.race(iterable)
第一个 Promise 有结果(无论成功/失败),就立刻返回那个结果。适合设置超时竞赛。
js
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('超时')), 5000)
);
Promise.race([fetch('/api/data'), timeout])
.then(res => res.json())
.catch(err => console.error(err));
5. Promise.allSettled(iterable)
等待所有 Promise 都完成(无论成功或失败),返回一个对象数组,每个对象包含 status 和 value/reason。非常适合不互相依赖的请求,且想知道每一个的结果。
js
Promise.allSettled([fetch('/a'), fetch('/b')])
.then(results => {
results.forEach(r => {
if (r.status === 'fulfilled') {
console.log('成功:', r.value);
} else {
console.log('失败:', r.reason);
}
});
});
6. Promise.any(iterable)
只要有任一 Promise 成功,就返回那个成功的值;如果全部失败,才返回一个 AggregateError(包含所有错误) 。适合某个节点故障,希望快速切换到可用服务。
js
Promise.any([fetch('/server1'), fetch('/server2')])
.then(res => res.json())
.catch(err => console.log('全部服务器都挂了', err.errors));
| 方法 | 核心逻辑 | 返回时机 | 失败处理 |
|---|---|---|---|
| Promise.all | 全员成功才算成功 | 等所有完成 | 一个失败,整体立即失败 |
| Promise.race | 谁先完成用谁(不论成败) | 第一个出结果就返回 | 第一个失败就整体失败 |
| Promise.allSettled | 等全员完成,保留成败信息 | 全部完成后返回 | 永远不失败,每个结果都保留 |
| Promise.any | 只要有一个成功就行 | 第一个成功就返回 | 全部失败才报错 |
八、错误处理:集中捕获与穿透机制
Promise 的错误处理非常优雅:任何一个 .then 里抛出的错误(同步或异步),都会顺着链向下穿透,直到遇到最近的 .catch。这让你可以把错误处理集中放在末尾。
推荐做法:
只在链的最后放一个 .catch。中间 .then 只专注成功逻辑。
如果需要继续传递错误,可以在 .catch 里 throw 或返回一个 Promise.reject()。
js
doSomething()
.then(step1)
.then(step2)
.catch(globalErrorHandler);
万一有些错误你不希望中断链,可以在局部捕获并处理,然后返回一个"恢复值":
js
doSomething()
.then(step1)
.catch(err => {
console.warn('step1 失败,使用默认值');
return defaultValue; // 继续传递给后面的 then
})
.then(step2);
九、执行顺序与微任务
Promise 的回调是微任务(Microtask),它比 setTimeout 这样的宏任务优先级更高。同步代码 -> 微任务队列 -> 宏任务队列。
js
console.log('1 同步');
setTimeout(() => console.log('2 宏任务'), 0);
Promise.resolve().then(() => console.log('3 微任务'));
console.log('4 同步');
// 输出顺序: 1 -> 4 -> 3 -> 2
理解这一点对调试异步代码非常重要。
十、Promise 与 async/await:语法糖的魔力
ES2017 引入的 async/await 本质上是 Promise 的语法糖,让你用"同步风格"写异步代码。
async 函数自动返回一个 Promise。
await 会暂停函数执行,等待右侧的 Promise 完成,并直接拿到 resolve 的值。
错误处理可以用 try...catch 包裹。
改写之前的"冰箱拿可乐"例子:
js
async function getCola() {
try {
const msg1 = await wait(1000, '第一步:打开冰箱');
console.log(msg1);
const msg2 = await wait(1000, '第二步:拿出可乐');
console.log(msg2);
const msg3 = await wait(1000, '第三步:喝可乐');
console.log(msg3);
return '喝完了';
} catch (err) {
console.error('中间出错了', err);
}
}
getCola().then(finalMsg => console.log(finalMsg));
注意:await 只能在 async 函数内部使用。虽然代码看起来像同步,但并不会阻塞主线程,底层依然是 Promise 和微任务。
十一、常见误区与注意事项
忘记 return
链式调用时,如果在 .then 里启动了新的异步操作但没有 return 那个 Promise,下一个 .then 会立刻执行,拿不到异步结果。
js
fetchData()
.then(data => {
process(data); // 没 return
})
.then(() => console.log('可能拿不到数据'));
// 应改为 return process(data);
executor 立即执行
new Promise(executor) 里的 executor 是同步执行;但 resolve/reject 可以通过异步延迟调用。
状态凝固后无法改变
多次调用 resolve/reject 只有第一次生效。
.catch 之后可以继续链
.catch 返回一个新的 Promise,默认是 fulfilled(除非又抛错误),所以后面还可以 .then。
避免"漂浮的 Promise"
对于不需要等待结果的 Promise,也要适当处理错误,否则可能会默默吞掉异常。可以加上 .catch(() => {})。
性能考虑
Promise 本身有内存开销,极度频繁地创建短生命周期的 Promise 可能影响性能,不过在绝大多数场景下这不是瓶颈。
十二、总结
Promise 是 JavaScript 异步编程的核心机制,它通过状态机模型和链式调用,彻底解决了回调地狱,让异步代码变得清晰、可维护。掌握以下要点,你就完全拿下了 Promise:
三种状态:pending、fulfilled、rejected,不可逆。
核心方法:then / catch / finally,返回新 Promise,链式调用。
静态方法:all / race / allSettled / any / resolve / reject。
错误处理:集中捕获,穿透机制。
微任务执行顺序:优于宏任务。
async/await:基于 Promise 的语法糖,写起来更舒服。
现在,你已经可以自信地放飞 Promise,写出优雅、健壮的异步代码了。下次团队 code review 的时候,同事看到你那干净利落的链式调用,说不定会默默给你点个赞呢!