自己动手实现Promises/A+规范
Promise并不是一个新的概念,它已经有将近30年的历史.
其早期的雏形还有里氏替换原则的提出者Barbara Liskov的贡献在其中.
https://en.wikipedia.org/wiki/Futures_and_promises#History
而Promises/A+这个规范的出现,则为JavaScript世界中众多Promise实现库提供了一套统一的API和交互机制.
Promises/A+提供了配套的测试集:https://github.com/promises-aplus/promises-tests.
其中共有872个测试,如果你的实现能够让全部测试绿起来,则可以认为该实现符合了标准.
我的Promise实现:https://github.com/cuipengfei/Spikes/tree/master/js/promise
在npm上的发布:https://www.npmjs.com/package/RWPromise
要实现Promises/A+的规范其实并不需要很多代码,我的实现只有88行.当然,仅仅是符合规范和一个可用,易用的Promise库之间还有很大的差距.
如果作为教学或者演示的目的,我认为我的这份实现是已有实现中最简洁的一版.
自己实现Promise规范时需要注意的几点:
1. promise的状态一旦确定,不可更改
一个符合规范的promise有三种可能的状态:pending,resolved,rejected。
这三者是互斥的。
一个pending的promise可以变成resolved,或者rejected。
但是一旦进入resolved或者rejected状态,就再也不能变了。
用形象的语言来描述的话:一个promise就是一个关于未来的承诺,诺言一旦履行,不能反悔。
假设有如下代码:
1 | var p = ???();//首先以某种方式拿到一个promise,假设这个promise现在是pending的 |
时间流逝,假设???()方法内部在未来某个不确定的时间执行了:
1 | p.resolve(); |
然后,你的x函数应该会被调用。
再然后,无论p的resolve方法或者reject方法再怎么被调用,p的状态都不会再变更,x和y也再不会被执行了。
2. 树状结构
对then方法的多次调用会形成一个树状的数据结构。
假设有如下代码:
1 | var p = ???();//首先以某种方式拿到一个promise |
上述代码等价于:
1 | var p = ???();//首先以某种方式拿到一个promise |
当然,这个代码形成的会是类似于一个链表的结构,可以把它看作是树状结构的一个特例,也就是树中每个节点都最多只有一个子节点。
而如下的代码则会形成我们惯常看到的树:
1 | var p = ???(); |
这时,树中每一个节点可以有任意多的子节点(取决于它的then被调用了多少次)。
了解promise的树状结构,将有助于实现promise时在自己脑子里构造递归模型。
3. 回调的执行时机
这是实现promise的时候,最容易把人搞晕的一点。
1 | var p = ???();//首先以某种方式拿到一个promise,假设这时p是pending的状态 |
以上代码执行完之后,我们手里有3个promise:p,p1,p2.
这时,a,b,c,d都还没有执行。
在未来某个不确定的时间,如果p的resolve方法被调用了,接下来会发生的事情是:
- p会把传给resolve方法的参数value记住,并把自己的状态标记为resolved (以后就再也不能变了)
- a会被调用到,其参数为value
- 如果a执行过程中不出错
- p1的状态被变成resolved,p1会把a的返回值记住
- c会被调用到,其参数为a的返回值
- 如果c执行过程中不出错
- p2的状态被变成resolved,p2会把c的返回值记住
- 如果c执行过程中出错
- p2的状态被变成rejected,p2会把c抛出的异常记住
- 如果c执行过程中不出错
- 如果a执行过程中出错
- p1的状态被变成rejected,p1会把a抛出的异常记住
- d会被调用到,参数为a抛出的异常
- 如果d执行过程中不出错
- p2的状态被变成resolved,p2会把d的返回值记住
- 如果d执行过程中出错
- p2的状态被变成rejected,p2会把d抛出的异常记住
- 如果d执行过程中不出错
- 如果a执行过程中不出错
这样,就看出递归的意思来了。不过b并没有在上面出现,这是因为p本身是被resolve的,b只有在p被reject的时候才会执行。
在未来某个不确定的时间,如果p的reject方法被调用了,接下来会发生的事情是:
。。。 。。。
就不用写了,把上面的a替换为b就好了。
以上的例子中,我们拿到p的时候它的状态是pending的,我们会先调用p的then,然后p才会被resolve(或者reject掉)。
也就是说当我们通过调用then传递给promise两个回调的时候,promise还没有能力确定应该执行哪个回调,只有当未来promise自己被resolve或者reject了的时候,它自己的状态确定了,它才知道该挑哪一个回调来执行。
还有另一种可能性,那就是当你拿到p的时候p就已经被resolve(或者reject掉了),这时如果你再调用then方法的话,所传入的两个回调,到底哪个应该被调用,马上就可以决定了。
也就是说回调被调用的触发点一共有三个,then,resolve,reject这三个方法。