Promise 的五个状态,90% 的人只说对了三个

47 阅读5分钟

上次面试,面试官问:"Promise 有几个状态?"我想都没想:"三个嘛,pending、fulfilled、rejected。"面试官笑了笑:"只有三个吗?"我当场愣住,一脸懵。后来深挖了一圈才发现,Promise 的生命周期远不止表面那三个,还有两个隐藏阶段几乎没人提。

Promise 的三种基础状态

Promise 本质上是个状态机,规范里定义了三种状态:

  • pending(进行中):初始状态,既没成功也没失败
  • fulfilled(已成功):操作完成,有结果值
  • rejected(已失败):操作失败,有拒绝原因

关键规则:状态转换不可逆。pending 只能转 fulfilled 或 rejected,一旦变了就不能再变。fulfilled 和 rejected 之间也不能互转。

先看一个有问题的"手写 Promise":

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,完全违反了不可逆原则。

正确写法要加状态锁:

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 会立即执行回调,不用等微任务队列。

const p = new Promise(resolve => resolve(42)) // 同步 resolve

p.then(val => {
  console.log(val) // 立即进入回调,不用等下一轮事件循环
})

被忽略的两个"隐藏状态"

严格来说,ECMAScript 规范里只有上面那三种状态。但规范里有个词经常被忽略:settled(已定态)。

一个 Promise 从 pending 变成 fulfilled 或 rejected,就叫"settled"。它不是一个你能通过 API 查到的状态,但在理解 Promise 行为时特别有用。

看一个典型的坑:

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(链式态)。每次调 thencatchfinally 都会返回一个全新的 Promise,形成链式结构:

// 错误写法:链式调用中丢失了状态
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操作失败能,catchthen 第二个参数
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 翻车的例子:

// 问题代码:一个接口慢,全部卡住
const results = await Promise.all([
  fetch('/api/users'),    // 200ms
  fetch('/api/orders'),   // 300ms
  fetch('/api/config')    // 这个接口偶尔超时,直接全部 reject
])
// config 一挂,users 和 orders 的数据也拿不到

allSettled 就能避免这个问题:

// 正确写法:用 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 的数据

再看 raceany 的区别,很多人搞混:

// 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,但对状态的处理完全不同:

// 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 一个值,以为能改变结果。

// 问题代码: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 的坑?评论区聊聊。