1. 从一个噩梦般的bug说起:为什么要研究Promise A+
如果你写前端写过一两年,大概率遇到过我这样的经历:用axios请求一个接口,然后 .then(res => ...) 回调里数据拿不到,页面白屏一片;或者两个 Promise.all 套着用,某个分支里抛了个异常,整个链路都不走了,但控制台只给了一句泛泛的 "Unhandled promise rejection"。
最坑的一次,我排查了整整一天,最后发现是引用的两个第三方库各自实现了Promise,一个用原生Promise,另一个用的是老版本的 bluebird,这两个实例的 then 互不认账,回调链直接断掉。那种感觉就像两个人的方言都说的是"吃饭",但一个说普通话一个说客家话,谁也听不懂谁。
Promise的互操作性问题,就是这个叫 Promise A+ 的规范想解决的事情。它不规定Promise有什么用、能不能取消、怎么去调度微任务——这些全是后来的Promise标准去补充的。A+做的只有一件事:把 then 方法的行为规则抠到极致,抠到任何一套Promise实现只要遵守这套规则,就能跟其他任意遵守规则的实现完美协作。
这篇文章,我把这套规范和它背后的核心思想,用最直白的方式拆开来讲。你不需要预先精通任何Promise源码,只要写过 .then(),有一点点"回调"的概念,就能看懂。适合谁看:被Promise链搞到怀疑人生的前端;面试前想彻底搞懂Promise底层机制的求职者;以及想尝试手写Promise源码、但被各种版本的实现绕晕的开发者。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Promise A+到底在规范什么:一份只谈then的互操作协议
先别急着钻进代码。理解任何标准,第一步是理解它的边界:它管什么,不管什么。Promise A+最反直觉的地方就在于——它的全部内容几乎都在说 then。
2.1 规范的全名里藏着它的野心
Promise A+的完整名称是 "Promise/A+ Specification",它是在早期Promise/A、Promise/B等提案的基础上修订出来的一个开放标准。为什么叫"A+"?因为在它之前有一个"Promise/A"规范,那套规范设计得比较粗略,不同实现之间对 then 的语义理解不一致,导致同一个Promise代码,换一个库跑起来结果就不一样。A+就是要做那个"加强修正版"。
这个规范的制定过程非常公开,它其实是社区里一批工程师从各自Promise库的实践中提炼出来的共识。jQuery的Deferred、Q、When.js、Bluebird、原生Promise,后来的实现基本都向A+靠拢。这意味着你现在写的原生Promise代码,拿给任何一个遵守A+的库去消费,行为都是可预期的。
2.2 A+不管的那些事,为什么不管
A+管得极其克制。它不规定:
- Promise怎么创建(是
new Promise(...)还是别的构造方式) - 怎么实现
.catch、.finally、.all、.race这些方法 - 回调是异步执行还是同步执行(虽然它要求必须异步,但它没规定用宏任务还是微任务)
- 未捕获的rejection怎么上报
- 怎么取消Promise
你可能会问:这也不管那也不管,那要它干嘛?
这里的关键认知是:A+的目的是 标准化then方法,让不同的Promise实现可以互相操作。所有的Promise实例,无论来自哪个库,最终都靠 then 来传递值和状态。只要 then 的行为统一,上层的一切方法(catch、all、race)都是个人自由。原生Promise比A+多了很多方法,但这些方法内部的基石,依然是严格遵循A+规则的 then。
打个比方:A+就像统一了电源插头的规格。至于插头上接的是电饭煲还是洗衣机,标准不关心,但你只要用这个规格的插头,任何标准的插座都能插。
3. 状态机模型:Promise为何必须是"单向流动"的水
Promise的核心是一个极其简单的状态机。三个状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。规则也只有两条:
- 状态只能从 pending 变到 fulfilled 或 rejected
- 状态一旦改变,就永远不可逆
这个看起来人畜无害的规则,是整个Promise设计哲学的地基。
3.1 为什么要"单项流动"而不是"可逆"
很容易忽略的是,这个"不可逆"特征,恰好是Promise能避免回调地狱最关键的机制。
想象一下,如果Promise状态可以来回切换,会是什么场景?你在 .then 里拿到一个值,正准备处理,结果Promise从 fulfilled 又跳回 pending,重新发起一次运算,那你上一次拿到的值还算数吗?代码的确定性就没了。而回调地狱之所以可怕,就是因为在回调嵌套中,状态和时序是纠缠在一起的,你根本说不清某个数据是哪个时刻的。
Promise用"状态一旦定型就永久固定"这种粗暴但无比有效的方式,把时间问题转化为了空间问题。一个 fulfilled 的Promise,你任何时候去 then 它,拿到的都是同一个值,顺序无关、时间无关。这就像一份公证过的合同,内容一旦盖章,什么时候查阅都一样。
3.2 一个 fulfilled 的Promise,then 注册的时机没有任何影响
这里有一个常见的误解:必须先有结果,然后再 then 才有意义。实际上,A+规定 then 可以在Promise处于任何状态时调用:
- pending 时调用:回调会排队,等状态落定后执行
- fulfilled 时调用:当前轮直接执行成功回调
- rejected 时调用:当前轮直接执行失败回调
有意思的场景是,一个已经 fulfilled 的Promise,你调用 then,回调并不立刻同步执行,而是异步调度。A+的2.2.4条要求:onFulfilled 和 onRejected 必须"直到执行上下文栈只包含平台代码时"才被调用。翻译成人话:then里面的回调永远不可能是同步的。你写 Promise.resolve(1).then(console.log); console.log(2),输出顺序一定是 2 然后 1。
这条规则为的就是消除"有时同步有时异步"的时序不确定性。如果 then 的回调有时同步有时异步,程序执行的先后顺序就依赖状态,这会让代码的行为在并发和嵌套场景下变得完全不可预测。
3.3 解释Promise A+中的术语:value、reason、thenable
在深入细节之前,先把规范里几个高频词统一口径:
- value:成功状态下的值,可以是任意合法的JavaScript值,包括
undefined、对象,甚至另一个Promise - reason:失败状态下的原因,通常是一个Error对象,但理论上可以是任何值
- thenable:任何带有
then方法的对象或函数
A+真正的精妙之处都在处理"value是一个thenable"时该怎么办——这也是下一节的核心。记住这个词:"thenable"。
4. then方法的完整规则:Promise A+的主战场
A+文档里,关于 then 的规则占了几乎一大半的篇幅。这里的每一条,拆开看都能发现它是在回答一个具体场景下的问题。
4.1 then 必须返回一个Promise,这是Promise链得以存在的基石
A+的2.2.7条,在我看来是整个规范中最具分量的一条:
then必须返回一个Promise。
为什么这条至关重要?因为如果 then 不返回新的Promise,你就没法链式调用,代码会退回回调嵌套的老路。但仅仅是"返回一个Promise",远没有听起来那么简单——关键在于,**
