遵循 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】。文章转载请联系作者。
评论