手搓 Promise(下):A+ 之外的 API
- # javascript
- # promise
- # async
上一篇《手搓 Promise(上):Promise/A+ 核心语义》里,我把注意力放在 Promise/A+ 的核心语义上:状态只能改变一次,then 返回新的 promise,回调结果要经过 Promise Resolution Procedure,thenable 需要被同化。
这一篇接着往外看。
配套学习地址:https://zxq2023.github.io/promise_learnring/step/13.html。
A+ 规范没有规定完整的 JavaScript Promise API。Promise.resolve、Promise.reject、catch、finally、Promise.all、Promise.race 这些方法都不属于 A+ 的核心内容。但它们也不是另一套独立机制,而是围绕 then、状态流转和 resolution procedure 组织出来的使用体验。
所以看这些 API 时,我更关心一个问题:它们分别是在 A+ 核心之上补了什么能力?
Promise.resolve:把输入统一成 promise 语义
Promise.resolve 最容易被误解成“创建一个 fulfilled promise”。
这个说法只对普通值大致成立。如果传入的是 thenable,它不能简单地 fulfilled 为这个 thenable 对象,而是要继续走 resolution procedure,采用 thenable 的最终状态。
所以我更愿意把 Promise.resolve 理解成:把一个输入值放进 Promise 的解析系统里。
这个输入可以是普通值,也可以是一个 promise,也可以是一个只有 then 方法的 thenable。经过 Promise.resolve 之后,调用方就不需要再关心输入原本是什么形态,后续都可以按 promise 的方式接着处理。
这也是很多组合 API 的基础。只要每个输入都先经过 Promise.resolve,普通值、原生 Promise、自定义 thenable 就能被统一到同一套链式语义里。
Promise.reject:直接制造 rejected 状态
Promise.reject 比 Promise.resolve 直接得多。它的目标不是解析输入,而是创建一个 rejected promise。
这里有一个细节:reject 不会同化 thenable。因为 rejection 的 reason 本身就应该被原样保留。
也就是说,如果把一个 thenable 作为拒绝原因传给 Promise.reject,这个 thenable 不会被展开,也不会被继续跟随。它只是一个 reason。
这和 Promise.resolve 正好形成对照:
resolve面向解析,要尝试采用 thenable 的最终状态。reject面向拒绝,要保留原始 reason。
这个区别很重要。否则错误原因也会被隐式展开,rejection 的语义就会变得不稳定。
catch:then 的错误分支语法糖
catch 本质上是 then 的语法糖。
它只注册 rejected 回调,不注册 fulfilled 回调。理解这一点后,错误传播就会简单很多:catch 并不是 Promise 链里的特殊通道,它仍然返回一个新的 promise,也仍然遵守 then 的返回值解析规则。
这意味着 catch 里可以做三件事:
- 返回一个普通值,让链条从 rejected 恢复为 fulfilled。
- 抛出一个新错误,让链条继续 rejected。
- 返回另一个 promise,让后续状态跟随这个 promise。
所以 catch 不是“链条结束”的标志。它只是当前这一段链路对 rejection 的处理节点。处理完之后,后面还可以继续 then,也可以继续 catch。
finally:不改变原结果的清理节点
finally 的重点不是接收 fulfilled value 或 rejected reason,而是无论前面成功还是失败,都执行一段清理逻辑。
我会把它理解成 Promise 链里的“旁路清理节点”。
它默认不改变原来的结果。前面是 fulfilled,清理之后继续 fulfilled;前面是 rejected,清理之后继续 rejected。只有当 finally 自己抛错,或者它返回的 promise rejected,后续链条才会改用这个新的 rejection。
这个语义很适合放加载状态、资源释放、临时标记清理这类逻辑。
finally 之所以能做到这一点,靠的仍然是 then。它只是同时处理 fulfilled 和 rejected 两条分支,并在清理完成后把原来的 value 或 reason 继续传下去。
Promise.all:等待全部完成
Promise.all 是组合多个输入的工具。
它的规则可以概括成一句话:全部 fulfilled,整体才 fulfilled;只要有一个 rejected,整体就 rejected。
这里有两个关键点。
第一,输入不一定都是 promise。它们可能是普通值,也可能是 thenable。所以 all 内部需要先把每一项统一成 promise 语义。
第二,输出结果需要保持输入顺序,而不是完成顺序。某一项先完成,并不意味着它就排在结果数组前面。结果位置应该对应输入位置。
all 的价值在于表达“这些任务是一个整体”。只有所有任务都成功,后续逻辑才有意义;任何一个任务失败,整体就应该进入错误分支。
Promise.race:采用第一个 settle 的结果
Promise.race 也是组合工具,但它关心的不是全部完成,而是谁先 settle。
这里的 settle 包括 fulfilled 和 rejected。也就是说,第一个完成的是成功结果,整体就 fulfilled;第一个完成的是失败结果,整体就 rejected。
它能成立,依赖的仍然是 Promise 的单次状态语义。多个输入都可能先后 settle,但外层 promise 只能采用第一次结果,后面的结果会被忽略。
所以 race 适合表达“只要第一个结果”的场景。它不关心其他输入最后如何,只关心谁先把外层 promise 的状态确定下来。
这些 API 的共同底座
把这些 API 放在一起看,会发现它们并没有离开 A+ 核心太远。
Promise.resolve 依赖 resolution procedure,把不同形态的输入统一成 promise 语义。
catch 和 finally 都是围绕 then 组织出来的链式体验。
Promise.all 和 Promise.race 依赖状态只能改变一次,也依赖 Promise.resolve 对输入做统一解析。
所以 A+ 之外的这些 API,可以理解成更高层的组合和使用便利。底层真正支撑它们的,仍然是上一篇讲到的几件事:状态、then、异步回调、resolution procedure,以及 thenable 同化。
先把这层关系理清楚,后面再真正动手写代码时,每个方法要做什么就会清晰很多。