function INTERNAL() {}
var REJECTED = ['REJECTED'];
var FULFILLED = ['FULFILLED'];
var PENDING = ['PENDING'];
var handlers = {}
function Promise (resolver) {
if (typeof resolver !== 'function') {
throw new TypeError('resolver must be a function');
}
this.state = PENDING;
this.queue = [];
this.outcome = void 0;
/* istanbul ignore else */
if (!process.browser) {
this.handled = UNHANDLED;
}
if (resolver !== INTERNAL) {
safelyResolveThenable(this, resolver);
}
}
Lie源码解读
这篇文章是通过
lie.js
的源码一起去了解下如何实现Promise
相关的规范。首先是
Promise
的核心的构造函数的实现。构造函数内部定义了几个
promise
实例的属性:promise
的状态值.有3种:rejected
,fulfilled
,pending
queue数组
用以保存这个promise
被resolve/rejected
后需要异步执行的回调这个
promise
实例的值对于promise,我们一般的用法是:
给这个
Promise
构造函数内部传入的resolver
由内部的方法safelyResolveThenable
去执行:在
safelyResolveThenable
方法中设定了一个called
标志位,这是因为一旦一个promise的状态发生了改变,那么之后的状态不能再次被改变,举例:如果给
Promise
构造函数传入callback
在执行过程中没有报错,且被resolve
的话,那么这个时候即调用的onSuccess
方法,这个方法内部调用了handlers.resolve
方法。接下来我们看下这个方法的定义:
再回到我们上面举的这个例子:
在这个例子当中,是同步去
resolve
这个promise
,那么返回的这个promise
实例的状态便为fulfilled
,同时outcome
的值也被设为1
。将这个例子拓展一下:
在实例的
then
方法上传入一个onFullfilled
回调执行上面的代码,最后在控制台输出1
。接下来我们看下
Promise
原型上then
方法的定义:在
then
方法内部首先创建一个新的promise
,接下来会根据这个promise
的状态来进行不同的处理。如果这个
promise
已经被resolve/reject
了(即非pending
态),那么会直接调用unwrap()
方法来执行对应的回调函数;如果这个
promise
还是处于pending
态,那么需要实例化一个QueueItem
,并推入到queue
队列当中。我们首先分析第一种情况,即调用
then
方法的时候,promise
的状态已经被resolve/reject
了,那么根据对应的state
来取对应的回调函数,并调用unwrap
函数(后面会详细讲解这个方法)。在这个函数中,使用
immediate
方法统一的将func
方法异步的执行。并将这个func
执行的返回值传递到下一个promise
的处理方法当中。因此在上面给的例子当中,因为
Promise
的状态是被同步resolve
的,那么接下来立即调用then
方法,并执行传入的onFullfilled
方法。第二种情况,如果
promise
还是处于pending
态,这个时候不是立即执行callback
,首先实例化一个QueueItem
,并缓存到这个promise
的queue
队列当中,延迟执行这个queue
当中保存的回调函数。QueueItem
构造函数接受3个参数:promise
,onFullfilled
,onRejected
。在
then
当中新创建的promise对象上一个promise被resolve后需要调用的回调
上一个promise被reject后需要调用的回调函数
接下来我们看下第二种情况是在什么样的情况下去执行的:
在这个例子当中,当过了
3s
后在控制台输出1
。在这个例子当中,因为promise
内部是异步去resolve
这个promise
。在这个promise
被resolve
前,promise
实例通过then
方法向这个promise
的queue
队列中添加onFullfilled
方法,这个queue
中保存的方法会等到promise
被resolve
后才会被执行。当在实际的调用resolve(1)
时,即promise
这个时候才被resolve
,那么便会调用handlers.resolve
方法,并依次调用这个promise
的queue
队列当中保存的onFullfilled
函数可以看到在
QueueItem
函数内部,会对onFullfilled
和onRejected
的参数类型做判断,只有当它们是函数的时候,才会将这个方法进行一次缓存,同时使用otherCallFulfilled
方法覆盖原有的callFulfilled
方法。这也是大家经常会遇到的值穿透的问题,举个例子:最后在控制台打印出2,而非3。当上一个
promise
被resolve
后,调用这个promise
的queue
当中缓存的queueItem
上的callFulfilled
方法,因为then
方法接收的是数值类型,因此这个queueItem
上的callFulfilled
方法未被覆盖,因此这时所做的便是直接将这个queueItem
中保存的promise
进行resolve
,同时将上一个promise
的值传下去。可以这样理解,如果then
方法第一个参数接收到的是个函数,那么就由这个函数处理上一个promise
传递过来的值,如果不是函数,那么就像管道一样,先流过这个then
方法,而将上一个值传递给下下个then
方法接收到的函数去处理。上面提到了关于
unwrap
这个函数,这个函数的作用就是统一的将then
方法中接收到的onFullfilled
参数异步的执行。主要是使用了immediate
这个库。这里说明下为什么统一的要将onFullfilled
方法进行异步话的处理呢。首先,是要解决代码逻辑执行顺序的问题,首先来看一种情况:
这个
promise
可能会被同步的resolve
,也有可能异步的resolve
,这个时候如果onFullfilled
方法设计成同步的执行的话,那么foo
及bar
的执行顺序便依赖promise
是被同步or异步被resolve
,但是如果统一将onFullfilled
方法设计成异步的执行的话,那么bar
方法始终在foo
方法前执行,这样就保证了代码执行的顺序。其次,是要解决同步回调
stackoverflow
的问题,具体的链接请戳我我们看到
lie.js
的内部实现当中,每次在调用then
方法的时候,内部都新创建了一个promise
的对象并返回,这样也完成了promise
的链式调用。即:需要注意的是,在每个
then
方法内部创建的新的promise
对象的state
为pending
态,outcome
为null
。可以将上面示例的promise
打印到控制台,你会非常清晰的看到整个promise
链的结构:实际这个
promise
链是一个嵌套的结构,一旦的最外部的promise
的状态发生了改变,那么就会依次执行这个promise
的queue
队列里保存的queueItem
的onFulfilled
或者onRejected
方法,并这样一直传递下去。因此这也是大家经常看到的promise
链一旦开始,就会一直向下执行,没法在哪个promise
的执行过程中中断。不过刚才也提到了关于在
then
方法内部是创建的一个新的pending
状态的promise
,这个promise
状态的改变完全是由上一个promise
的状态决定的,如果上一个promise
是被resolve
的,那么这个promise
同样是被resolve
的(前提是在代码执行过程中没有报错),并这样传递下去,同样如果上一个promise
是被rejected
的,那么这个状态也会一直传递下去。如果有这样一种情况,在某个promise
封装的请求中,如果响应的错误码不符合要求,不希望这个promise
继续被resolve
下去,同时想要单独的catch
住这个响应的话,那么可以在then
方法中直接返回一个被rejected
的promise
。这样在这个promise
后面的then
方法中创建的promise
的state
都会被rejected
,同时这些promise
所接受的fullfilled
方法不再执行,如果有传入onRejected
方法的话便会执行onRejected
方法,最后一直传递到的catch
方法中添加的onReject
方法。看完
lie
的源码后,觉得promise
设计还是挺巧妙的,promise
事实上就是一个状态机,不过状态值能发生一次转变,由于then
方法内部每次都是创建了一个新的promise
,这样也完成了promise
的链式调用,同时then
方法中的回调统一设计为异步执行也保证了代码逻辑执行顺序。