前情提要:之前为手动实现 Promise 搭建了环境,然后还基于 Promises/A+ 规范实现了一个简单的 Promise。这次本文就接着上面的补充,实现一个复杂的 Promise。
2.2.7
2.2.7 then must return a promise
2.2.7 promise2 = promise1.then(onFulfilled, onRejected);
这里就是 Promise.then
必须返回一个 Promise
测试用例
it('2.2.7 then必须返回一个promise', () => { |
源码
class PromiseComplex { |
这里需要做的操作就是 then
函数中需要返回一个 Promise
即可,注意这里的 Promise
是自己的写的 PromiseComplex
2.2.7.1
2.2.7.1 If either
onFulfilled
oronRejected
returns a valuex
, run the Promise Resolution Procedure[[Resolve]](promise2, x)
Promises/A+ 规范的 2.2.7.1 里面这句话什么意思?就是说,如果 onFulfilled
或者 onRejected
函数返回了一个 x 的值, 那么就运行 Promise 的 Resolution 步骤,即运行 [[Resolve]](promise2, x)
云里雾里的,其实这句话的意思就是,在 promise
内部执行 resolve
或者 reject
函数时,会调用一个成功或者失败的函数,这个函数会返回一个值叫 x
,然后再把这个值 x
跟一个新的 promise
传给一个叫做 Resolve
的函数。这个叫做 Resolve
的函数干了什么事情呢,它会将这个值 x
变成 then
里面传的回调的参数。这么说可能不太直观,接着看下面的测试用例应该会明白一些。
测试用例
it(`2.2.7.1 如果 then(success, fail) 中的 success 返回一个值 x, |
源码
那么测试用例有了,源码应该怎么考虑呢?答案很简单,跟着规范来写。首先我们这里因为是用类写的,所以我们可以使用 Promise.Resovle(x)
的方式来表达 2.2.7.1 规范中出现的调用方式。这里可以将 Resolve
这个函数的名字改动下,我们叫它 resolveWith
吧,然后在源码中定义 resolveWith
class PromiseComplex { |
因为定义调用 resolveWith
的方式是通过 Promise.resolveWith
的方式调用的,而这个 Promise
根据规范是一个 promise2
,也就意味着这是一个新的 Promise
,还记得之前源码中的 then
中的 return new PromiseComplex(() => {})
么,现在我们这么做
class PromiseComplex { |
这里将这个新的 Promise
存在 handle
这个数组的第三个元素中,然后再返回就好。接下来就需要对 resolve
以及 reject
中改写下逻辑了
class PromiseComplex { |
接下来就是 重头戏 了,因为要实现这一条规范,或者说通过上面的测试用例,还需要结合 规范 2.3.x 来看,因为 2.3.x 的规范是教你怎么写上面说到的 resolveWith
函数的
resolveWith
为了实现 resolveWith
,必须遵循以下步骤(规范)
2.3.1
If promise and x refer to the same object, reject promise with a TypeError as the reason
如果 promise 和值 x 引用的是同一个对象,则用 TypeError 作为 reason 拒绝(reject)promise,因为之前 resolveWith
函数是通过 nextPromise.resolveWith(x)
这种方式调用的,所以这里规范说的 promise 就是指这个代码中的 nextPromise
,而在 resolveWith
函数中,nextPromise
就是 this
。即为了实现 2.3.1 规范, resolveWith
函数应该这么写
resolveWith(x) { |
2.3.2
如果 x 是一个 promise,采取它的状态
2.3.2.1 If x is pending, promise must remain pending until x is fulfilled or rejected.
2.3.2.2 If/when x is fulfilled, fulfill promise with the same value.
2.3.2.3 If/when x is rejected, reject promise with the same reason.
上面的规范,说白了就是,如果 x 是一个 promise,就采取 promise 的状态,所以就在 resolveWith
函数中加判断条件
resolveWith(x) { |
2.3.3
否则,如果 x 是一个对象或者函数
2.3.3.1 Let
then
bex.then
.
这句话的意思就是代码,即声明一个 then
变量,将 x.then
赋值给这个变量
2.3.3.2 If retrieving the property
x.then
results in a thrown exceptione
, rejectpromise
withe
as the reason.
如果 x.then
抛出一个异常,则使用 e
作为 reason 并 reject 这个 promise
2.3.3.3 If
then
is a function, call it withx
asthis
, first argumentresolvePromise
, and second argumentrejectPromise
, where:
如果 then
是一个方法,把 x
当作 this
来调用它, 第一个参数为 resolvePromise
,第二个参数为 rejectPromise
2.3.3.3.1 If/when
resolvePromise
is called with a valuey
, run[[Resolve]](promise, y)
.
如果 resolvePromise
调用时传了参数 y
,则执行 resolveWith(y)
2.3.3.3.2 If/when
rejectPromise
is called with a reasonr
, rejectpromise
withr
如果 rejectPromise
调用时传了参数 r
,则执行 resolveWith(r)
(这里也可以直接调用 this.reject(r)
函数,为了程序的统一性,这边改动成了 resolveWith(r)
)
2.3.3.3.3 If both
resolvePromise
andrejectPromise
are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored
如果 resolvePromise
和 rejectPromise
都被调用了,或者对同一个参数进行多次调用,则以第一次调用为优先级最高,其他未来的调用会被忽略掉。这里解释下这个情况,就是比方说如下的代码,可以说明 2.3.3.3.3 中所说的例子
const promise = new PromiseComplex((resolve, reject) => resolve({ |
或者
const promise = new PromiseComplex((resolve, reject) => resolve({ |
或者
const promise = new PromiseComplex((resolve, reject) => { |
上面的代码,多次进行了 resolve
的操作,但是只取第一次的,所以最终打印出来的就是 '123'
。由于该功能已经在上一篇博客文章中解决,所以这里的规则可以自动忽略掉
2.3.3.3.4 if calling
then
throws an exceptione
2.3.3.3.4.1 IfresolvePromise
orrejectPromise
have been called, ignore it.
2.3.3.3.4.2 Otherwise, rejectpromise
withe
as the reason.
如果调用 then
的时候抛出异常了
- 如果
resolvePromise
和rejectPromise
的都被调用了,就忽略它 - 否则,reject 掉这个
promise
2.3.3.4 If
then
is not a function, fulfillpromise
withx
.
如果 then
不是一个函数,则 resolve 掉这个 promise
根据以上的描述,resolveWith
中的代码可以增加为
resolveWith(x) { |
2.2.7.2
2.2.7.2 If either
onFulfilled
oronRejected
throws an exceptione
,promise2
must be rejected withe
as the reason.
如果在调用 resolve
或者 reject
时抛出了异常,则 nextPromise
必须调用 reject
,并将这个 e
作为参数传入
测试用例
it('2.2.7.2 如果 success 或 fail 抛出一个异常 e, promise2 必须被拒绝', done => { |
源码
由规范的定义可知,这里就需要在 resolve
以及 reject
函数中,在调用成功或者失败函数时,需要加个 try catch
,因为在调用这些函数的时候,有可能会失败
class PromiseComplex { |
API 实现
以上,基本就是实现了一个复杂的符合 Promises/A+ 规范的 Promise 了,接下来就是实现该 Promise 的其他 API 的工作了
resolve/reject
真正的 Promise 中是能直接访问到这个方法的,而我们这里实现的 PromiseComplex
是一个类,如果要能访问到这个方法,则必须在方法上加上 static
属性
测试用例
这里另起一个 describe
,专门用来测试该 Promise
的 API 用。之后的各种 API 相关的测试用例都在这个里面写
describe('Promise API', () => { |
源码
简便起见,这里直接使用
resolve2
和reject2
来命名这两个方法
class PromiseComplex { |
可以看到,基本上就是调用了类里面的逻辑,new 一个 Promise,然后调用里面的 resolve
或者 reject
方法。但是对于 resolve
来说,如果 result 中有 then,并且这个 then 是一个函数,则需要继续调用这个 result.then
catch
用来捕获 Promise reject 的错误用的,这里实现的逻辑也很简单
测试用例
it('测试 catch', done => { |
源码
class PromiseComplex { |
注意这里 catch
的参数 reject
是一个失败函数
finally
测试 finally
这个 API 的关键就是看 finally
中传的函数有没有得到执行,所以测试用例可以写成下面这样
测试用例
it('测试 finally', () => { |
源码
因为无论成功或者失败,都会走到 finally
中,所以 finally
都可以继续 then
class PromiseComplex { |
这里有个疑问,为何 finally
不能做如下的实现
finally(callback) { |
是因为假如我们想要再其调用失败函数的时候报错,比如需要做一个 throw an error
之类的操作,这个时候你只能通过 Promise.resolve
之类的方式去实现它。而为了能够跟失败函数被调用时代码逻辑一致,所以成功函数也写成跟失败函数类似的逻辑。
all
all API 的逻辑看起来简单,就是遍历执行,但是内部是做了一些情况的判断的,不过测试用例还是很简单的,如下
测试用例
it('测试 all, 等待所有都完成(或第一个失败) ', done => { |
源码
先说下实现 all API 需要注意的几个点
- 如果传入的参数是一个空的可迭代对象,那么此 promise 对象回调完成(resolve), 只有此情况是同步执行的,其它都是异步返回
- promises 中所有的 promise 都“完成”时,或参数中(result)不包含 promise 时回调完成。
- 如果传入的参数(promises[i])不包含任何 promise,则返回一个异步完成
- 如果参数中有一个 promise 失败,那么 Promise.all 返回的 promise 对象失败
- 在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组
所以实现的逻辑如下
class PromiseComplex { |
allSettled
allSettled API 在 MDN 的解释如下
The
Promise.allSettled()
method returns a promise that resolves after all of the given promises have either fulfilled or rejected, with an array of objects that each describes the outcome of each promise.
即
Promise.allSettled
方法会返回一个 promise,then 中传的函数的参数就是一个对象数组,这个对象数组描述了每个 promise 的输出情况,这个 promise 能够 resolve 所有给到的 promises,无论这些 promises 是被 fulfilled 了还是被 rejected
比如如下的代码
const promise1 = Promise.resolve(3); |
输出就是如下
> Object { status: "fulfilled", value: 3 } |
测试用例
it('测试 allSettled', done => { |
源码
源码的逻辑就是在 all
API 的基础上做修改,注意这里返回的状态
- 如果成功,那么每个 promise 产出的结果应该是
{ state: 'fulfilled', result: 123 }
- 如果失败,那么每个 promise 产出的结果应该是
{ state: 'rejected', reason: 456 }
所以应该这么写
class PromiseComplex { |
但是这里其实还有一种更加优雅的写法,直接调用 Promise.all
的形式来实现
static allSettled(promises) { |
这里将 promises 做了一个 map,然后它返回的是一个数组 x(promises)
,代码就变得更加精简和易读了。你可能会迷惑,这里为啥要用到一个 promise.then
对其做一个值的构建呢?因为我们这里,then
是会返回一个 promise 的,通过使用 .then()
这种方式来修改 promise 中的传值参数才是一种符合 promise 的方式。
race
race 在 MDN 上的解释如下
The
Promise.race()
method returns a promise that fulfills or rejects as soon as one of the promises in an iterable fulfills or rejects, with the value or reason from that promise.
即
Promise.race()
方法返回一个 promise,一旦其中一个 promise 处于一个可迭代的 fulfills 或者 rejects 中时,那这个对应的 promise 就会 fulfill 一个 value 出来或者 reject 一个 reason 出来
简单来说就是 竞争,哪个先执行 resolve 或者 reject,哪个就先导出对应的结果
测试用例
上面可能说的不是很清楚,先看下测试用例吧
it('测试 race', done => { |
以上的测试用例,由于 promise2 持续的时间最短,那么按照道理来说,promise2 的结果就是最终的结果
源码
race API 源码实现思路也是通过遍历来实现,但是这里是通过调用 Promise.resolve(xxx).then
来实现的。只要其中有一个 resolve
或者 reject
了,那么就自动走 then
的逻辑,其他的就忽略掉。
static race(promises) { |
你可能会疑惑了,为啥这里可以自动走 then
的逻辑呢?这又牵扯到 微任务宏任务 的知识了。先说结论,在 eventLoop 中,一旦执行一个阶段里的一个宏任务(setTimeout, setInterval 和 setImmediate)就立刻执行微任务队列(Node 11 及以上或者浏览器环境中),而我们代码中的 resolve
方法(不是 resolve2
方法) 是放在微任务队列中的(下文优化代码部分会说到)。所以它会先执行 PromiseComplex.resolve2(promises[i])
(宏任务),在下一个宏任务(promise2
中的 resolve
)到来之前,执行微任务(resolve
中的代码逻辑),发现状态没有变(state === 'pending'
)就往下执行,将状态改变,并且将 nextTick
中的代码入微任务队列。然后时间到,执行宏任务(promise2
中的 resolve
),然后执行微任务(resolve
中的代码逻辑),发现状态变了(state !== 'pending'
),后面的逻辑就不执行了。这个时候因为你已经将 then
中的 callback
当作参数传给了 Promise
内部的变量,所以就可以拿着这些 callbacks
去执行。这样看起来就是 then
中的逻辑被 自动执行 了
你可能又会疑惑了,为啥这里可以忽略掉其他的 promise
呢?还记得规范 2.3.3.3.3 中说的是啥么(手动滑稽),以及结合规范 2.2.2 和 2.2.3 来看的话,关键的地方在于,这个 promise
内部的 state
改变了,一旦这个 state
被改变了,那么无论后面怎么 resolve
或者 reject
,那么都会在判断 state
状态时被 return 掉
resolve (result) { |
重构 resolve/reject API
在之前的这两个 api 的逻辑中,关于异步的部分只是用了 setTimeout
来解决的,但是这里就会存在一个 bug,在测试用例中,要断言 assert 测试代码的结果,也需要用到 setTimeout
,但是这里需要延时一小段时间,不然会和源码中的 setTimeout
有冲突(即如果测试用例中 setTimeout
传的时间是 0 ms,那么有可能这个会优先源码中的 setTimeout
执行),这是一个 bug 隐患。那么解决方式是什么呢?很简单,将 setTimeout
修改成 process.nextTick
(微任务,优先级更高) 即可
resolve(result) { |
但是这样又会带来新的问题,如果是浏览器环境就不支持 process.nextTick
,所以这里就需要想办法做一个兼容的处理。怎么兼容呢,这里可以参考 Vue 源码中关于 nextTick
是怎么处理的。这里方便起见就直接上代码了
// 兼容 process.nextTick 和 setImmediate 方案 |
这里是通过 MutationObserver 这个 API,通过创建一个 textNode 的节点,然后使用 observer.observe
去 监听这个节点的变化,节点一旦变化就会去执行传入的 fn
回调,这样就可以做一个兼容的处理了。
最后只需要将 resolve/reject
API 中的 process.nextTick
改写成 nextTick
函数即可
总结
以上就是实现一个复杂的 Promise
的完整步骤了,仓库链接可以点击这里,另外我还在这个项目中对部分代码进行了优化,但总体上的逻辑和本文都是一样的,若有看到源码不一样的同学还请不要惊讶
本质上来说,这里主要就规范中的 2.2.7 以及 2.3 的部分进行了比较细的描述,如果有问题欢迎各位大佬在项目中提 issue