🤔

彻底搞懂 JavaScript Promise:从“外卖小票”说起

在 JS 里,Promise 就像一张“外卖小票”:你现在下单,结果(送达/取消)要过一会儿才知道;但你可以先把“送到之后要做什么”“取消之后要做什么”提前写好,等结果一出来,系统自动按规则执行。

理解 Promise,不只是为了会写 then/catch,更是为了写出可读、可控、不乱套的异步代码。


1. 为什么需要 Promise?

先回忆一下没有 Promise 的时代:异步全靠回调。

比如:请求用户信息 → 再请求订单 → 再请求物流。你很快就会写出这样的代码:

  • 层层嵌套(俗称“回调地狱”)
  • 错误处理分散、重复
  • 执行顺序难推导(尤其混入定时器/事件)

Promise 的核心价值是:把“异步的结果”抽象成一个对象,并提供统一的链式写法,让异步代码像同步一样“顺序叙述”。


2. 类比:外卖小票 = Promise 对象

把 Promise 想象成你在外卖平台下的一单:

  • 你下单的那一刻,订单状态是 pending(进行中)
  • 可能送达:fulfilled(已成功)
  • 也可能取消/失败:rejected(已失败)

一旦从 pending 变成 fulfilled/rejected,状态就不可逆,并且结果会被“记住”(缓存):

  • fulfilled 会记住一个值(value)
  • rejected 会记住一个原因(reason / error)

这就是 Promise 的“承诺”:我现在给你一张票,未来一定给你一个明确结果,并且只给一次。


3. Promise 的基本用法:把“之后做什么”写清楚

3.1 创建 Promise:你负责“下单流程”

const order = new Promise((resolve, reject) => {
  // 异步动作:比如 1 秒后出结果
  setTimeout(() => {
    const ok = Math.random() > 0.3
    if (ok) resolve('外卖送达:炸鸡+可乐')
    else reject(new Error('商家接单失败'))
  }, 1000)
})
  • resolve(value):标记成功
  • reject(err):标记失败

3.2 消费 Promise:你负责“送达/失败后怎么处理”

order
  .then((food) => {
    console.log('开吃:', food)
  })
  .catch((err) => {
    console.log('今晚饿着:', err.message)
  })
  .finally(() => {
    console.log('不管怎样,记得收拾桌子')
  })

4. 关键机制:链式调用到底在链什么?

很多人以为 .then() 只是“注册回调”,但更重要的是:

每一次 then/catch/finally 都会返回一个新的 Promise。

这意味着你可以把流程拆成多个步骤,一步一步写:

fetchUser()
  .then(user => fetchOrders(user.id))
  .then(orders => fetchLogistics(orders[0].id))
  .then(logistics => console.log('物流信息:', logistics))
  .catch(err => console.error('任一步失败都会到这里:', err))

规则速记(非常重要)

  1. then 的返回值决定下一步

    • 返回普通值 → 下一步走 fulfilled,值就是返回值
    • 返回 Promise → 下一步会“接管”它的状态
    • 抛出错误 / 返回 rejected Promise → 下一步走 rejected
  2. 错误会沿链传递,直到被 catch 接住

    • 所以你不必每一步都 try/catch

5. “你会预测顺序吗?”:Promise 与事件循环的关系

Promise 的回调(then/catch/finally)属于微任务

看这段代码,按输出顺序推导一下:

console.log('1. 同步开始')

setTimeout(() => {
  console.log('4. 定时器(宏任务)')
}, 0)

Promise.resolve()
  .then(() => console.log('3. then(微任务)'))

console.log('2. 同步结束')

正确顺序:

  1. 1. 同步开始
  2. 2. 同步结束
  3. 3. then(微任务)
  4. 4. 定时器(宏任务)

记忆点:同步 → 清空微任务 → 再轮到宏任务


6. 实战:Promise.all / allSettled / race / any 怎么选?

把它们想成“多单外卖的处理策略”。

6.1 Promise.all:必须全都送达,否则整单失败(短路)

Promise.all([p1, p2, p3])
  .then(([r1, r2, r3]) => {})
  .catch(err => {})
  • 适合:依赖全部结果才能继续(比如页面初始化的多个关键请求)
  • 特点:任何一个失败就直接 reject

6.2 Promise.allSettled:不管成功失败都给我汇总报表

const results = await Promise.allSettled([p1, p2, p3])
// [{status:'fulfilled', value:...}, {status:'rejected', reason:...}]
  • 适合:允许部分失败(比如批量请求,能展示多少算多少)

6.3 Promise.race:谁先有结果就用谁(成功/失败都算)

Promise.race([timeout(1000), fetchData()])
  • 适合:超时控制、竞速策略

6.4 Promise.any:谁先成功就用谁(失败会等到都失败)

Promise.any([cdn1(), cdn2(), cdn3()])
  .then(res => {})
  .catch(err => {
    // AggregateError:全部失败
  })
  • 适合:多线路容灾(“哪个节点先通用哪个”)

7. 常见坑:你以为的 Promise,可能不是你以为的 Promise

A. try/catch 抓不到异步里的错误

try {
  setTimeout(() => {
    throw new Error('炸了')
  }, 0)
} catch (e) {
  console.log('抓不到', e)
}

原因:throw 发生在另一个宏任务里。

正确方式:让异步以 Promise 形式暴露错误,再 .catch()await try/catch

B. 忘了 return,链就断了

doStep1()
  .then(() => {
    doStep2() // 忘 return:外层不会等它
  })
  .then(() => {
    console.log('可能过早执行')
  })

修正:

doStep1()
  .then(() => {
    return doStep2()
  })
  .then(() => {
    console.log('按顺序执行')
  })

C. “Promise 构造器反模式”:能不用 new Promise 就别用

如果你已经有 Promise(比如 fetch())或能用 async,不要再手写包一层 new Promise,否则更容易漏掉 reject、漏 return。


8. 总结:Promise 的“承诺精神”

Promise 的本质可以压缩成三句话:

  • 状态只会从 pending → fulfilled/rejected,并且不可逆
  • then/catch/finally 会返回新 Promise,从而实现链式流程控制
  • then 回调是微任务:同步完立刻执行,优先于定时器

如果你愿意,我也可以按这篇文章的风格继续写一篇“async/await = Promise 的语法糖,但糖里有哪些牙齿(坑)”的续集,并配一组“预测输出顺序”的练习题。