崔鹏飞的Octopress Blog

自己动手实现Promises/A+规范

| Comments

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
2
var p = ???();//首先以某种方式拿到一个promise,假设这个promise现在是pending的
p.then(x,y);//然后把你希望在成功和失败时执行的x,y通过then方法挂进去

时间流逝,假设???()方法内部在未来某个不确定的时间执行了:

1
p.resolve();

然后,你的x函数应该会被调用。

再然后,无论p的resolve方法或者reject方法再怎么被调用,p的状态都不会再变更,x和y也再不会被执行了

2. 树状结构

对then方法的多次调用会形成一个树状的数据结构。

假设有如下代码:

1
2
3
4
var p = ???();//首先以某种方式拿到一个promise
p
    .then(a,b) //假设这次then的调用返回的是一个新的promise实例,称之为p1
    .then(c,d);//假设这次then的调用返回的是一个新的promise实例,称之为p2

上述代码等价于:

1
2
3
var p = ???();//首先以某种方式拿到一个promise
var p1 = p.then(a,b);
var p2 = p1.then(c,d);

当然,这个代码形成的会是类似于一个链表的结构,可以把它看作是树状结构的一个特例,也就是树中每个节点都最多只有一个子节点。

而如下的代码则会形成我们惯常看到的树:

1
2
3
4
5
6
7
var p = ???();
var p1 = p.then(a,b);
var p2 = p.then(c,d);
var p3 = p.then(e,f);

var p4 = p1.then(g,h);
var p5 = p3.then(i,j);

这时,树中每一个节点可以有任意多的子节点(取决于它的then被调用了多少次)。

了解promise的树状结构,将有助于实现promise时在自己脑子里构造递归模型。

3. 回调的执行时机

这是实现promise的时候,最容易把人搞晕的一点。

1
2
3
var p = ???();//首先以某种方式拿到一个promise,假设这时p是pending的状态
var p1 = p.then(a,b);
var p2 = p1.then(c,d);

以上代码执行完之后,我们手里有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抛出的异常记住
    • 如果a执行过程中出错
      • p1的状态被变成rejected,p1会把a抛出的异常记住
      • d会被调用到,参数为a抛出的异常
        • 如果d执行过程中不出错
          • p2的状态被变成resolved,p2会把d的返回值记住
        • 如果d执行过程中出错
          • p2的状态被变成rejected,p2会把d抛出的异常记住

这样,就看出递归的意思来了。不过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这三个方法。

Comments