彻底搞懂 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))
规则速记(非常重要)
-
then 的返回值决定下一步
- 返回普通值 → 下一步走 fulfilled,值就是返回值
- 返回 Promise → 下一步会“接管”它的状态
- 抛出错误 / 返回 rejected Promise → 下一步走 rejected
-
错误会沿链传递,直到被 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. 同步开始2. 同步结束3. then(微任务)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 的语法糖,但糖里有哪些牙齿(坑)”的续集,并配一组“预测输出顺序”的练习题。