Promise 使用与原理:then 链、微任务与组合方法(面试向)
Promise 不是“让代码变快”的魔法,它解决的是:把一次异步操作的最终结果(成功值/失败原因)抽象成一个可组合的对象,从而让你可以用链式写法、错误传播与并发组合,把异步逻辑写得更可控、更可推理。
面试速答(30 秒 TL;DR)
- Promise 表示“未来某个时刻才会得到的单次结果”,内部是三态状态机:
pending→fulfilled或rejected(只会变化一次)。 new Promise(executor)里的executor同步执行;但then/catch/finally的回调 一定异步,以**微任务(microtask)**方式调度。then永远返回一个新的 Promise:回调return什么,新 Promise 就“跟着变成什么”(返回普通值→成功;throw→失败;返回 Promise/thenable→同化其状态)。- 组合方法:
Promise.all:全成功才成功,任一失败就失败(失败快)Promise.allSettled:全收集,不会短路Promise.race:第一个“落定”(成功或失败)就结束Promise.any:第一个成功就成功;全失败抛AggregateError
- Promise 不是线程;“并发”来自你同时发起了多少 I/O(请求/读写/定时器),而不是 Promise 自己并行计算。
1. Promise 是什么:把“未来的结果”变成对象
你可以把 Promise 理解为一个带状态的盒子:
- 盒子一开始是
pending(未完成) - 未来某一刻变成
fulfilled(value)或rejected(reason) - 你可以提前在盒子上“登记回调”(
then/catch/finally),等它落定后自动按规则执行
- resolve:把“结果”交给 Promise 处理(它可能是普通值,也可能是另一个 Promise/thenable)。
- fulfill:Promise 最终以“成功值”落定。
- reject:Promise 最终以“失败原因”落定。
2. 基本用法:then/catch/finally 与 async/await
2.1 then():拿到成功值并继续链式处理
fetch('/api/user')
.then((res) => res.json())
.then((user) => {
console.log('user:', user);
return user.id;
})
.then((id) => fetch(`/api/user/${id}/profile`))
.then((res) => res.json())
.then((profile) => console.log('profile:', profile));
2.2 catch():统一处理链上的错误
catch(onRejected) 等价于 then(undefined, onRejected)。
doSomething()
.then(() => doNext())
.catch((err) => {
console.error('出错了:', err);
});
2.3 finally():无论成功失败都做收尾(不会“吃掉”结果)
finally 的回调不接收 value/reason;它的典型用途是“关 loading / 释放资源 / 打点”。
loadData()
.finally(() => setLoading(false))
.then(render)
.catch(showError);
关键规则(面试高频):
finally(() => 123)返回值会被忽略,不会替换原来的 value/reason- 但如果
finally抛错或返回一个失败的 Promise,会让链变成失败
Promise.resolve('OK')
.finally(() => Promise.reject('boom'))
.then(console.log)
.catch(console.log); // boom
2.4 async/await:Promise 的语法糖
async function getUser() {
const res = await fetch('/api/user');
return res.json();
}
getUser().then(console.log);
语义映射:
async function一定返回 Promisereturn x等价于return Promise.resolve(x)throw e等价于return Promise.reject(e)
3. 核心原理:为什么 then 能“接着写”
3.1 executor 同步,回调微任务:执行顺序题的核心
console.log('A');
new Promise((resolve) => {
console.log('B');
resolve('C');
}).then((v) => console.log(v));
console.log('D');
输出顺序是:
A
B
D
C
原因:
executor立即执行,所以先打印Bthen回调进入微任务队列,要等当前调用栈清空后才执行
如果你对“微任务/宏任务/事件循环”想系统补齐,可以看:docs/frontend/javascript/event-loop.md。
3.2 then 为什么会返回“新的 Promise”
then 的本质不是“在原 Promise 上继续改”,而是:基于旧 Promise 的结果,创建一个新 Promise(链条上的下一环)。
3.3 Promise Resolution Procedure:同化 thenable(原理的“最硬核”部分)
面试经常问:为什么 return 一个 Promise,链就会“等它”?
因为 then 在内部会对回调的返回值 x 做一套“吸收/同化”流程(简化描述):
你只需要记住这句口述版:
then返回的新 Promise(P2),会根据回调返回值来决定自己怎么落定:
返回值是普通值就成功;抛错就失败;返回 Promise/thenable 就“跟着它走”。
4. 组合方法:all/allSettled/race/any 怎么选
| API | 何时 fulfilled | 何时 rejected | 典型用途 |
|---|---|---|---|
Promise.all(list) | 全部成功,值为结果数组 | 任意一个失败(失败快) | 并行请求,要求“缺一不可” |
Promise.allSettled(list) | 永远成功,值为状态数组 | 仅当传参非法等异常 | 需要“收集全部结果(含失败)” |
Promise.race(list) | 第一个落定是成功 | 第一个落定是失败 | 超时、竞速、取最快响应 |
Promise.any(list) | 任意一个成功 | 全部失败(AggregateError) | 只要有一个可用就行(多源兜底) |
4.1 Promise.all:并行 + 失败快
const [user, profile] = await Promise.all([getUser(), getProfile()]);
4.2 Promise.race:实现超时
function timeout(ms) {
return new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), ms));
}
await Promise.race([fetch('/api/data'), timeout(3000)]);
race 只决定“谁先落定”;落定之后,其他 Promise 并不会自动取消。需要“真的取消”,请用 AbortController(如果底层 API 支持)。
5. 典型题 & 标准答法(带口述要点)
Q1:new Promise 里的代码是同步还是异步?
答法: executor 同步执行;then/catch/finally 回调异步执行(微任务)。
Q2:then 链里 return 和不 return 有啥区别?
答法: then 返回的新 Promise 要靠你的 return 来接住下一步;不 return 等价于返回 undefined,下一步拿到的就是 undefined,并且不会等待你在回调里“偷偷开启”的异步操作。
Q3:Promise.all vs allSettled vs any 怎么选?
答法:
- 全成功才行:
all - 全部都要统计(不管成功失败):
allSettled - 只要一个成功即可(多源兜底):
any
Q4:为什么 try/catch 抓不到 Promise 链里的错误?
答法: 因为 Promise 回调在微任务里执行,已经不在当前同步调用栈;要用 .catch() 或在 async/await 里用 try/catch 包住 await。
6. 常见追问
6.1 Promise 能取消吗?
Promise 本身不能取消。 常见做法:
- 通过
AbortController取消底层操作(如fetch) - 自己做“忽略结果”或“竞速超时”封装(逻辑取消,而非底层取消)
6.2 浏览器为什么会报 “Unhandled Promise Rejection”?
当一个 Promise 被 reject 且在“合适的时机”还没有挂上拒绝处理器(catch 或 then 的第二参),宿主环境会触发未处理拒绝的告警/事件。工程里建议:所有链都要有兜底 catch,或在更上层统一捕获与上报。
7. 易错点 / 坑(高频踩雷清单)
- Promise 构造器反模式:能用现成 Promise/
async就别new Promise再包一层(容易漏resolve/reject、吞错)。 forEach+async:forEach不会等await,要用for...of或Promise.all(map(...))。- 忘了
return:then(() => fetch(...))必须return fetch(...)才能让后续等待它。 finally误解:finally不会拿到 value/reason;返回值默认不会替换链上的结果。race当取消用:race不等于取消;超时后请求可能仍在进行,需要AbortController配合。
8. 速记要点(可背诵)
- 三态:
pending→fulfilled/rejected,只变一次 - executor 同步;
then/catch/finally微任务 then产出新 Promise:return值/抛错/返回 Promise 决定下一环all缺一不可;allSettled全收集;race取第一个落定;any取第一个成功