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

上次面试,面试官问:"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 (链式态)。每次调 thencatchfinally 都会返回一个全新的 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 操作失败 能,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 翻车的例子:

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 的数据

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

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 的坑?评论区聊聊。

相关推荐
小聪7081 小时前
elpis-core 抽离 npm 包过程的难点和卡点
前端·架构
钱栈up1 小时前
坯料管理导入功能bug修复:只改前端一个文件就解决问题
前端·bug
石小石Orz2 小时前
民间AI排行榜单新鲜出炉,Fable 5.1仅排第三
前端·后端·ai编程
FYKJ_20102 小时前
【毕设分享】基于Web的校园兼职信息发布与申请系统07059
前端·vue.js·spring boot·mysql·typescript·spark·课程设计
志尊宝3 小时前
Vue3 零基础每日笔记(032):Suspense 与异步组件——懒加载与顶层 await 终极方案
前端·javascript·vue.js·笔记·html5
Alice-YUE4 小时前
前端速通 Agent:5 个项目,一条学习路径
前端·大模型·ai agent·学习路径·deerflow
是立不是利4 小时前
前端分页逻辑与边界场景实战解析
前端·javascript
小聪7084 小时前
elpis-core 动态组件扩展设计
前端
Amos_Web4 小时前
Rspack 源码解析(七):Module、Chunk 与加载树优化
前端·rust·源码阅读