上次面试,面试官问:"Promise 有几个状态?"我想都没想:"三个嘛,pending、fulfilled、rejected。"面试官笑了笑:"只有三个吗?"我当场愣住,一脸懵。后来深挖了一圈才发现,Promise 的生命周期远不止表面那三个,还有两个隐藏阶段几乎没人提。
Promise 的三种基础状态
Promise 本质上是个状态机,规范里定义了三种状态:
- pending(进行中):初始状态,既没成功也没失败
- fulfilled(已成功):操作完成,有结果值
- rejected(已失败):操作失败,有拒绝原因
关键规则:状态转换不可逆。pending 只能转 fulfilled 或 rejected,一旦变了就不能再变。fulfilled 和 rejected 之间也不能互转。
先看一个有问题的"手写 Promise":
javascript
function BadPromise() {
this.status = 'pending'
this.value = null
}
BadPromise.prototype.resolve = function(val) {
// 没有检查当前状态,可以反复修改
this.status = 'fulfilled'
this.value = val
}
BadPromise.prototype.reject = function(err) {
// 已经 fulfilled 之后还能调 reject,状态被覆盖了
this.status = 'rejected'
this.value = err
}
问题在于:状态可以被反复修改,fulfilled 之后还能变 rejected,完全违反了不可逆原则。
正确写法要加状态锁:
javascript
function MyPromise(executor) {
this.status = 'pending' // 初始状态
this.value = null // 结果或原因
this.callbacks = [] // 存储 then 的回调
const resolve = (val) => {
if (this.status !== 'pending') return // 状态已变更,直接忽略
this.status = 'fulfilled' // 锁定为成功
this.value = val
this.callbacks.forEach(cb => cb.onFulfilled(val))
}
const reject = (err) => {
if (this.status !== 'pending') return // 同样加锁
this.status = 'rejected' // 锁定为失败
this.value = err
this.callbacks.forEach(cb => cb.onRejected(err))
}
try {
executor(resolve, reject) // 执行传入的函数
} catch (e) {
reject(e) // executor 抛错自动 reject
}
}
注意 if (this.status !== 'pending') return 这行,这就是状态锁的核心。第一次调用 resolve 或 reject 之后,后续调用全部被忽略。
还有一个容易忽略的点:then 对已定态的 Promise 会立即执行回调,不用等微任务队列。
javascript
const p = new Promise(resolve => resolve(42)) // 同步 resolve
p.then(val => {
console.log(val) // 立即进入回调,不用等下一轮事件循环
})
被忽略的两个"隐藏状态"
严格来说,ECMAScript 规范里只有上面那三种状态。但规范里有个词经常被忽略:settled(已定态)。
一个 Promise 从 pending 变成 fulfilled 或 rejected,就叫"settled"。它不是一个你能通过 API 查到的状态,但在理解 Promise 行为时特别有用。
看一个典型的坑:
javascript
async function tricky() {
const p = new Promise(resolve => {
resolve(Promise.resolve(1)) // resolve 传了一个 Promise
})
p.then(val => console.log(val)) // 你以为输出 1?
}
tricky()
// 实际输出确实是 1,但中间多了一轮等待
// 外层 Promise 会"等"内层 Promise 定态后才 resolve
这里外层 Promise 虽然调了 resolve,但因为传入的是另一个 Promise,外层会保持 pending 直到内层 settled。这就是"settled"这个概念的意义------Promise 的 resolve 不等于 settled。
另一个容易混淆的是 chained (链式态)。每次调 then、catch、finally 都会返回一个全新的 Promise,形成链式结构:
javascript
// 错误写法:链式调用中丢失了状态
fetch('/api/user')
.then(res => res.json())
.then(data => {
console.log(data)
// 这里如果抛错,下面的 catch 能接到
// 但如果上面 res.json() 失败了,data 就是 undefined
})
.catch(err => console.error(err)) // 能捕获上面所有环节的错
// 更稳的写法:每一步都处理好状态
fetch('/api/user')
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`) // 主动检查
return res.json()
})
.then(data => console.log(data))
.catch(err => console.error(err))
所以从工程角度看,Promise 的生命周期可以拆成五个阶段:
| 阶段 | 含义 | 能否通过 API 观察到 |
|---|---|---|
| pending | 初始,等待中 | 能,console.log 能看到 |
| fulfilled | 操作成功 | 能,then 的第一个参数 |
| rejected | 操作失败 | 能,catch 或 then 第二个参数 |
| settled | 已定态(fulfilled 或 rejected 的统称) | 不能直接观察,规范术语 |
| chained | 链式调用产生的新 Promise | 能,每次 then 的返回值 |
Promise 静态方法对状态的不同处理
Promise 有四个常用的静态方法,它们的核心区别就是对多个 Promise 状态的不同处理策略:
| 方法 | 成功条件 | 失败条件 | 是否短路 |
|---|---|---|---|
Promise.all |
全部 fulfilled | 任一 rejected 立即 reject | 是,遇到 reject 立刻停止 |
Promise.race |
第一个 settled 的结果 | 同左,看谁先 | 是,只取最快那个 |
Promise.allSettled |
全部 settled(不管成败) | 不会 reject | 否,等所有都定态 |
Promise.any |
任一 fulfilled | 全部 rejected 才 reject | 是,遇到 fulfilled 立刻返回 |
先看一个用 Promise.all 翻车的例子:
javascript
// 问题代码:一个接口慢,全部卡住
const results = await Promise.all([
fetch('/api/users'), // 200ms
fetch('/api/orders'), // 300ms
fetch('/api/config') // 这个接口偶尔超时,直接全部 reject
])
// config 一挂,users 和 orders 的数据也拿不到
用 allSettled 就能避免这个问题:
javascript
// 正确写法:用 allSettled 兜底
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/orders'),
fetch('/api/config')
])
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`接口${index}成功:`, result.value) // 正常消费
} else {
console.log(`接口${index}失败:`, result.reason) // 单独处理,不影响其他
}
})
// config 挂了也不影响 users 和 orders 的数据
再看 race 和 any 的区别,很多人搞混:
javascript
// race:取第一个 settled 的,不管成败
const fast = Promise.race([
fetch('/api/data'), // 假设 200ms
new Promise((_, reject) => // 100ms 后超时
setTimeout(() => reject(new Error('timeout')), 100)
)
])
// fast 会 reject,因为超时更快 ------ race 只看速度不看成败
// any:取第一个 fulfilled 的,reject 的直接跳过
const winner = Promise.any([
Promise.reject('server A down'), // 这个被跳过
Promise.resolve('server B ok'), // 取这个
Promise.reject('server C down') // 这个也被跳过
])
// winner 会 fulfill 为 'server B ok' ------ any 只看成功
一句话:race 是赛跑,谁先到算谁;any 是选拔,只要第一个成功的。
then、catch、finally 对状态的影响
这三个方法都会返回新 Promise,但对状态的处理完全不同:
javascript
// catch 之后状态会"翻转"
Promise.reject('err')
.catch(err => {
console.log(err) // 输出 'err'
return 'recovered' // 返回了一个 fulfilled 的值
})
.then(val => {
console.log(val) // 输出 'recovered',状态已经从 rejected 变成 fulfilled
})
// finally 不接收参数,也不改变状态
Promise.resolve('ok')
.finally(() => {
console.log('cleanup') // 不管成败都执行
// 注意:这里拿不到 'ok'
})
.then(val => {
console.log(val) // 还是 'ok',状态透传
})
一个常见的错误:在 finally 里 return 一个值,以为能改变结果。
javascript
// 问题代码:finally 里 return 没用
Promise.reject('error')
.finally(() => {
return 'ignored' // 这个返回值会被忽略
})
.catch(err => {
console.log(err) // 还是 'error',不是 'ignored'
})
// 正确做法:要改状态就在 catch 里改
Promise.reject('error')
.catch(err => {
console.log(err) // 'error'
return 'recovered' // 在这里改状态才有效
})
.then(val => {
console.log(val) // 'recovered'
})
finally 的设计初衷就是"不管结果如何,执行清理操作"。它不关心也不应该改变 Promise 的状态。
最后
快速回顾一下要点:
- Promise 规范定义了三种状态:pending、fulfilled、rejected
- 状态转换是单向的,一旦 settled 就不能再变
- "settled"是规范术语,指 Promise 脱离了 pending 状态
- "chained"是链式调用产生的新 Promise,每次
then都会产生 - 四个静态方法(all / race / allSettled / any)本质是对多 Promise 状态的不同组合策略
finally不接收参数,也不改变 Promise 状态
所以下次面试再被问到"Promise 有几个状态",你可以先说三个基础的,再把 settled 和 chained 补上,面试官应该会觉得你真的研究过。你面试时被问过哪些 Promise 的坑?评论区聊聊。