遵循 Promises/A+ 规范,手把手带你实现 Promise 源码 (核心篇)

前言
上几周小包一直在跟 Promise 较劲,不彻底学会 Promise 不罢休,Promise 不止用起来强大,学起来也是让人受益匪浅,Promise基础篇部分我们可以学会基础 Promise 功能,异步逻辑,链式调用的编写。Promise 静态四兄弟 部分我们可以学习 Promise 的四种静态方法 all、race、allSettled、any 的使用及其实现。
但 Promise 部分还缺少最核心的一环 —— Promise Resolution Procedure。那这个东西又是什么呐?
Promise Resolution Procedure
我们回忆下基础篇链式调用部分的代码,代码是这样处理的:
 
 then 方法中按照 promise 状态分成了三种情况,回调函数后的返回值 x,直接作为 promise 的成功值。
看到这里,你应该会产生疑惑,难道所有类型的返回值 x 都直接返回吗?
当然不是,Promises/A+ 针对返回值 x 有非常复杂的处理,这也就是本文要讲解的 The Promise Resolution Procedure。
我们从基础篇的实现代码也可以发现,三种情况的处理代码是高度类似的,如果再加入对返回值 x 多种情况的处理,代码不堪想象,因此我们将这部分逻辑抽离出来成 resolvePromise 方法。
resolvePromise 方法接受四个参数:
promise2 就是 then 方法返回值,x 是 then 回调函数的返回值,我们根据返回值 x 的情况,决定调用 resolve 或 reject。
返回值 x
在阅读 Promises/A+ 规范之前,我们先来思考一下返回值 x 会有那些情况:
- x是个普通值(原始类型)
- x是基于- Promises/A+规范- promise对象
- x是基于其他规范的- promise对象
- x就是- promise2(x 与 promise2 指向同一对象)
返回值 x 的情况是非常复杂的,我们接下来来看一下 Promises/A+ 规范是如何处理上述多种情况。
Promises/A+ 规范解读
针对 Promise Resolution Procedure 部分,Promises/A+ 规范洋洋洒洒的写了很大篇幅:
 
 下面咱们来理解上述规范到底讲述了什么。
Promise 解析过程是一个抽象操作,参数值为 promise 和 value,我们将其表示为
[[Resolve]](promise,x)
这里与上面讲述的 resolvePromise 是相通的,只不过为了代码编写,我们同时将 promise2 的 resovle 及 reject 方法作为参数传入。
如果 x 可 thenable,那么我们就认为 x 是一个类 promise 对象,它会尝试让 promise 采用 x 的状态。只要它们符合 Promises/A+ 的 then 方法,允许 promise 对 thenable 的处理进行互操作。
promise 有很多规范,Promises/A+ 只是其中之一,Promises/A+ 希望自身能兼容其他规范下的 promise 对象,判断依据为该对象是否可 thenable。
- 如果 - promise和- x引用的同一对象,则以- TypeError理由拒绝 (避免循环引用)
什么情况下会出现这种现象呐?看这样一个栗子:
- 如果 - x是一个- promise,采用它的状态
- 如果 - x状态是- pending,则- promise也需要保持- pending状态直至- x状态转变为- fulfilled或- rejected
- 如果 - x状态是- fulfilled,则以同样的值完成- promise
- 如果 - x状态是- rejected,则以同样的原因拒绝- promise
上面规范指出了当返回值 x 为 promise 对象时,我们应该如何处理,但并没有给出如何判断返回值 x 是否为 promise 对象
- x是一个对象或者函数
- 声明 - then其值为- x.then
- 如果检索属性 - x.then导致抛出异常- e,则以- e为拒绝原因拒绝- promise。
- 如果 - then是一个函数,- x作为- then的- this调用该方法,第一个参数是成功的回调函数,第二个参数是失败的回调函数—— 判断是否为 promise 的最小判断
- 如果成功回调以值 - y调用,运行- [[Resolve]](promise,y)
- 如果失败回调以原因 - r调用,用- r拒绝- promise
- 如果成功回调与失败回调都被调用或多次调用同一个参数,则第一个调用优先,其他调用都将被忽略。 
- 如果调用 - then方法抛出异常- e:
- 若成功回调或失败回调都调用过,忽略 
- 未调用,用 - e作为原因拒绝- promise
- 如果 - then不是函数,用- x作为值完成- promise
- 如果 - x既不是对象也不是函数,使用- x作为值完成- promise
我们已经解读规范完毕,小包下面提出几个问题,加深一下大家对 Promise Resolution 的理解。
问题
- 检索 - x.then属性会存在异常情况,你能举个类似栗子吗?
规范考虑的非常全面,由于 Promises/A+ 规范可以兼容其他具有 thenable 能力的 promise 实现,假设这样一个场景:
- 为什么 - then方法通过- call调用,而非- x.then调用?
then 属性已经被检索成功,如果再次检索,会存在一定风险。还是上面那个栗子,我们稍微改一下:
then.call(x) 与 x.then 效果相同,而且通过 then.call(x) 可以减少二次检索的风险。
- 如果成功回调以值 - y调用,运行- [[Resolve]](promise, y),这条规范啥意思?
小包想了很久,终于想通了这里,开始小包误以为此条规范针对了两种 onfulfilled 情况:
- onFulfilled函数返回- Promise实例
- onFulfilled函数执行时参数为- Promsie实例。
但经过对比思考,第二种情况是根本无法达到此规范。小包还是对第二种非常好奇,于是去反复翻阅了 Promises/A+ 规范,发现这竟然是规范的漏网之鱼,规范没有提到这点的处理。但我通过在浏览器进行尝试,发现对于第二种情况,ES6 同样会对此情况递归解析(有机会小包会单独写文章对比这两种情况)
对于 onFulfilled 返回 Promise 实例,小包来举个栗子:
从输出结果可以发现,p2 的 then 方法返回值为 p1 ,返回的是全新的 Promise 实例,与 p1 不同,只不过采用了 p1 状态。
- 如果成功回调与失败回调都被调用或多次调用同一个参数,则第一个调用优先,其他调用都将被忽略。这条规范又是在处理什么情况? 
看到这条规范,你可能会很奇怪,因为咱们手写的 Promise 在基础篇已经处理过当前情况,成功与失败回调只会调用其中之一。 Promises/A+ 规范中多次提到,可以兼容其他具备 thenable 能力的 promise 对象,其他规范实现的 promise 实例未必会处理此情况,因此此条规范是为了兼容其他不完善的 promise 实现。
源码实现
循环引用
如果 promise 和 x 引用的同一对象,则以 TypeError 理由拒绝。因此我们需要给 resolvePromise 添加一步校验:
判断 x 是否为 promise 实例
通过上面规范的解读,我们可以把判断 x 是否为 promise 实例归结为以下步骤:
- promise实例应该是对象或者函数: 首先判断- x是否为对象或函数
- promise对象必须具备- thenable能力: 接着检索- x.then属性
- then属性应该是个可执行的函数: 最后判断- then是否为函数 (这是最小判断)
- 如果上述都满足, - Promises/A+就认为- x是一个- promise实例
精炼一下: 首先判断 x 是否为对象或函数;然后判断 x.then 是否为函数
我们来编写一下这部分代码:
返回值 x 为 promise
上文我们已经对此条规范做了详细的解析,但应该如何实现此条规范呐?非常简单,我们只需对 then.call(x) 略作修改即可。
不知道大家能不能理解上述递归的原理?小包给举个栗子。
resolvePromise(promise, y, resolve, reject) 的执行流程是这样的:
- 经过一系列判断,最终通过 - y.then为函数判断出- y为- promise
- 执行 - then(y, resolvePromsie, rejectPromise)
- 上面代码等同于执行下面代码 
- y1的值是- 1,为普通值,因此直接调用- resolve(y1)
- 因此实现了 - promise2采纳返回值- x的状态
兼容不完善的 promise 实现
为了兼容不完善的 promise 实现,因此我们需要给 resolvePromise 中执行添加一个锁。
then 方法修改
文章最开始我们提到将 then 方法三种情况代码重复度过高,我们将此部分抽离为 resolvePromise ,第一个参数为 promise2。
我们取出基础篇 then 方法部分代码,重点关注 resolvePromise 调用部分。你应该很容易问题,promise2 是 then 整体执行完毕后才可以访问,resolvePromise 此时应该是无法访问到该方法。
因此我们需要给 resolvePromise 加一下异步操作,本手写使用 setTimeout 实现。
实现到这里,手写 Promise 就全部剧终了,下面我们来测试一下我们的手写 Promise 是否可以通过 Promises/A+ 提供的案例测试。
完整版 promise 代码: 手写 Promise 完全版
案例测试
延迟对象
在我们的手写 Promise 中添加 deferred 部分代码:
promises-aplus-tests
使用 npm 安装 promises-aplus-tests。
然后进入到待测试的 promise 文件夹,执行
 
 测试通过,大功告成! 手写 Promise 的部分就全部结束了,不知道你有没有收获很多。
后语
我是 战场小包 ,一个快速成长中的小前端,希望可以和大家一起进步。
如果喜欢小包,可以在 InfoQ 关注我,同样也可以关注我的小小公众号——小包学前端。
一路加油,冲向未来!!!
疫情早日结束 人间恢复太平
版权声明: 本文为 InfoQ 作者【战场小包】的原创文章。
原文链接:【http://xie.infoq.cn/article/e2483968019dd14138daa9708】。文章转载请联系作者。












 
    
评论