现在主流的浏览器都已经支持大部分的ES6的语法了,但是我们还是基本上会使用Babel将代码转成ES5的代码(可以结合webpack对polyfill进行动态引入),为了更好的兼容,我们还是会引入一些polyfill,今天主要是想梳理一下经常使用的Promise,就从这个promise-polyfill库进行分析吧。
请结合源码阅读
相对于 Promise/A+ 规范,这个库还实现了一些常用的Promise方法,比如: all、race和finally等,这些考虑放在另外一章进行梳理,这些相对比较容易。
篇幅很长,主要是担心自己漏掉细节,可能有些地方比较啰嗦重复。这章只是梳理了正常执行的流程,不包括异常的处理流程。那么下一节就会梳理 reject 的执行流程和一些原型链上的方法,例如
all、race和finally的实现等。
构造函数
阅读分析源码可以将代码下载下来,然后进行断点调试,多调几遍,核心的流程就可以分析完成了。接下来,就从断点开始吧。
下载源码
-
该库本来就是用来做兼容的,所有代码全都是
ES5的,所有直接在它的github上拷下来就行,当然也是可以通过npm安装的,这里就不多说了。源码地址 -
得到源码后,还需要修改一些构造函数挂载的全局变量名。因为主流的浏览器已经实现了
Promise了,直接在html页面中用script标签引入的话,它会判断浏览器中已经有了相应的Promise就不会把自己挂载上去了。
if (!('Promise' in globalNS)) { // 这里的 globalNS 在浏览器中就是 window
globalNS['Promise'] = Promise;
} else if (!globalNS.Promise.prototype['finally']) {
globalNS.Promise.prototype['finally'] = finallyConstructor;
}
我将globalNS['Promise'] 修改成 globalNS['MyPromise'],挂载到其他地方,最后如图:
- 在
html使用script标签引入代码,然后简单的使用MyPromise
...
<script src="./promise-ployfill.js"></script>
...
<script>
window.onload = function () {
var p = new MyPromise(function (resolve, reject) {
setTimeout(() => {
resolve('ok');
}, 3000)
});
p.then(function (value) {
console.log(value);
})
}
</script>
运行看看,会不会报错。
源码结构
通过 vscode 看看代码结构
这个 Promise 就是构造函数了,入口就在这里了。当我们使用 new MyPromise 就是调用这个构造函数来生成Promise实例了。
我们先分析一下代码然后在打断点进行验证。
Promise 构造函数
首先了解一下一个Promise有什么样的要求:
- promise 是有状态的,一个 promise 有且只有一个状态(pending,fulfilled,rejected 其中之一)。并且只能从 pending 状态到 fulfilled 状态或者 pending 状态到 rejected 状态。所有可以预知,必须有一个属性来表面或者控制状态。
- 一个 promise 必须提供一个 then 方法,用来获取当前或最终的 value 或 reason。一个 promise 的 then 方法接受两个参数:
promise.then(onFulfilled, onRejected),并且这两个参数都是可选的。
接下来看一下构造函数:
/**
* @constructor
* @param {Function} fn
*/
function Promise(fn) {
if (!(this instanceof Promise))
throw new TypeError('Promises must be constructed via new');
if (typeof fn !== 'function') throw new TypeError('not a function');
/** @type {!number} */
this._state = 0;
/** @type {!boolean} */
this._handled = false;
/** @type {Promise|undefined} */
this._value = undefined;
/** @type {!Array<!Function>} */
this._deferreds = [];
doResolve(fn, this);
}
- 首先判断调用这个构造函数是否使用
new关键字进行使用,否则报错。 - 再判断构造函数传入的
fn必须是一个function否则进行报错。 - 初始化
this._state = 0,这就是要求的状态的表示属性了。这个库的状态有四个值:0、1、2、4,前三个分别对应 pending,fulfilled,rejected,最后一个状态用来表示thenable(也就是调用 then)函数的resolve的值也是一个Promise实例,这个主要是用来转移状态。 - 初始化
this._handled = false,用来表面当前的 promise 示例是否已经被处理过。 - 初始化
this._value = undefined,用来记录resolve和reject传递的值。 - 初始化
this._deferreds = [];,用来存储then传入的方法,之后在 promise 从 pending 状态到 fulfilled状态(resolve后),会循环处理这些传入函数。 - 最后将传入的
fn函数,传入到doResolve方法中去执行。
doResolve 函数
/**
* Take a potentially misbehaving resolver function and make sure
* onFulfilled and onRejected are only called once.
*
* Makes no guarantees about asynchrony.
*/
function doResolve(fn, self) {
var done = false;
try {
fn(
function(value) {
if (done) return;
done = true;
resolve(self, value);
},
function(reason) {
if (done) return;
done = true;
reject(self, reason);
}
);
} catch (ex) {
if (done) return;
done = true;
reject(self, ex);
}
}
-
如官方的注释说的,这个函数主要是用来保证
resolve和reject只会被执行一次。之前说过 promise 的要求,状态只能变更一次,所有这里主要是防止执行多次。通过一个标志变量done来避免,代码还是比较好理解。 -
可以看到
fn函数被执行了,并且传入了两个函数,这里结合我们使用new MyPromise的代码看一下:
var p = new MyPromise(function (resolve, reject) {
setTimeout(() => {
resolve('ok');
}, 3000)
});
这里的 resolve 就等于:
function(value) {
if (done) return;
done = true;
resolve(self, value);
}
reject 就等于:
function(reason) {
if (done) return;
done = true;
reject(self, reason);
}
- 函数的执行被
try-catch包裹,就是说如果fn执行错误的话,就会直接导致 promise 调用reject,从 pending 状态变更到 rejected 状态了。
打断点先看看 promise 的构造函数的执行(gif)
我们先在如下几个地方打入断点:
- new MyPromise
- new MyPromise 传入的函数内部
- Promise 构造函数
- doResolve 内部
开始执行吧~~~
then 函数
当doResolve执行完成后,new MyPromise就会返回了一个 promise 对象了,然后我们立马又去调用了这个 promise 对象的 then 函数。看看这个函数干了写什么。
Promise.prototype.then = function(onFulfilled, onRejected) {
// @ts-ignore
var prom = new this.constructor(noop);
handle(this, new Handler(onFulfilled, onRejected, prom));
return prom;
};
-
首先调用了
this对象的constructor函数又创建了一个MyPromise对象,这里的this指的是当前的promise对象,constructor就指向的是Promise这个构造函数。注意这里的noop,它只是一个空函数,没有任何操作。 -
接着调用
handle函数,传入了两个参数,第一个是当前 promise 对象, 第二个是一个Handler实例。 -
最后将新创建的 promise 对象返回,这样就可以链式调用了。
Handler 对象
function Handler(onFulfilled, onRejected, promise) {
this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
this.onRejected = typeof onRejected === 'function' ? onRejected : null;
this.promise = promise;
}
这个对象也是相对比较简单,只是将then 传入的resolve和reject处理函数绑定在了当前对象上,这里注意一点,上一步then函数创建的新promise对象(noop创建的)也是绑定在了这个对象上。
handle 函数
看的到,then 函数关键的一步就是 handle 函数,把当前的 Promise 对象和 Handler 对象传入进行处理。
function handle(self, deferred) {
...
if (self._state === 0) {
self._deferreds.push(deferred);
return;
}
...
}
这里我们先只看到这一步,因为,在构造 Promise 对象时候,我们传入的函数,是使用了setTimeout回调调用的时候才去执行resolve,也就是成功回调后才去修改 promise 状态。所有我们执行then函数的时候,promise 对象的状态还是初始状态,还没有修改,所以then函数在还没有resolve的情况下,只是将传入的 Handler 对象存储到了 _deferreds 属性上。
那么到这里,我们可以发现,其实我们使用then函数,传入的处理函数都只是先存放在了一个任务队列中(在还没有调用resolve的情况下)。
resolve 函数
上面的几个步骤后,Promise 同步执行的操作就已经完了。之后的步骤就是等待resolve或者reject的执行了,所以接下来看看当我们setTimeout回调调用了resolve之后是个什么样子执行流程。
function resolve(self, newValue) {
try {
// Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
if (newValue === self)
throw new TypeError('A promise cannot be resolved with itself.');
if (
newValue &&
(typeof newValue === 'object' || typeof newValue === 'function')
) {
var then = newValue.then;
if (newValue instanceof Promise) {
self._state = 3;
self._value = newValue;
finale(self);
return;
} else if (typeof then === 'function') {
doResolve(bind(then, newValue), self);
return;
}
}
self._state = 1;
self._value = newValue;
finale(self);
} catch (e) {
reject(self, e);
}
}
还记得上面说的 fn这个传入函数么,那里传入了上面这个resolve,回顾一下:
function(value) {
if (done) return;
done = true;
resolve(self, value);
}
所以,newValues 就是setTimeout那里使用resolve调用传入的ok字符串。
- 判断
newValues是否是自身,如果是则报错。 - 如果
newValues是一个对象或者方法(构造函数)。- 是一个 Promise 实例,则将状态修改为
3,然后把值赋值给_value,再去执行finale(self)去完成最后的操作,这里先记住什么情况下 state 是被赋值为 3 的,之后在来分析。 - 如果这个对象不是一个 Promise 实例,但是有
then方法,则调用doResolve方法去直接执行这个then函数。
- 是一个 Promise 实例,则将状态修改为
- 上面两种情况都不成立,则将状态修改为
1,然后把值赋值给_value。最后调用finale(self)。
这里可以看到,状态是一定会进行变更了,然后再将实例丢给finale函数去执行。跟着流程我们接下来看看finale
finale 函数
function finale(self) {
if (self._state === 2 && self._deferreds.length === 0) {
Promise._immediateFn(function() {
if (!self._handled) {
Promise._unhandledRejectionFn(self._value);
}
});
}
for (var i = 0, len = self._deferreds.length; i < len; i++) {
handle(self, self._deferreds[i]);
}
self._deferreds = null;
}
- 如果状态为 2 或者没有处理函数,就把事情交给
_unhandledRejectionFn去处理了。 - 循环的执行使用
then注册的handler对象。之前说过,then传入的方法都会被放入到_deferreds中。
const p = new MyPromise(...)
p.then(() => { ... })
p.then(() => { ... })
p.then(() => { ... })
这里看到我们使用p执行了三个then,所以_deferreds就会有三个,finale会一次性全部循环处理完,注意这里与这样的链式调用是有区别的:
const p = new MyPromise(...)
p.then(() => { ... }).then(() => { ... }).then(() => { ... })
之前看过then方法会常见一份新的promise对象,然后返回,所以链式调用都是基于新的promise上。
好,接下来看一下handle函数:
function handle(self, deferred) {
while (self._state === 3) {
self = self._value;
}
if (self._state === 0) {
self._deferreds.push(deferred);
return;
}
self._handled = true;
Promise._immediateFn(function() {
var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
if (cb === null) {
(self._state === 1 ? resolve : reject)(deferred.promise, self._value);
return;
}
var ret;
try {
ret = cb(self._value);
} catch (e) {
reject(deferred.promise, e);
return;
}
resolve(deferred.promise, ret);
});
}
之前我把其他分支省略了,那么现在就是看其他分支的时候了。
- 当
resolve传入的是一个promise也就是该库定义的状态 3 时候,这里直接把self指向了 传入的promise。 - 状态是 0 就先过了,之前说过。
- 接下来就是标记
_handle,表示已经处理过。 - 接着将
cb,也就是我们传入的回调函数onFulfilled、onRejected,放到了_immediateFn方法里面去执行。
Promise._immediateFn =
// @ts-ignore
(typeof setImmediate === 'function' &&
function(fn) {
// @ts-ignore
setImmediate(fn);
}) ||
function(fn) {
setTimeoutFunc(fn, 0);
};
看到这个函数,只是兼容了不同环境的创建异步任务的方法,ES6里面Promise是 Microtasks (微任务),在ES5上,这里只能使用setTimeout创建一个 Macrotasks(宏任务),两者的区别有兴趣的同学去搜索一下吧。
根据_immediateFn函数我们就可以知道,finale函数将最后调用cb函数,并且是在下一个 Macrotasks 上,然后得到cb执行的结果,再去调用resolve(deferred.promise, ret);,这样就能继续处理后面新的promise,也就是这样循环往复的去调用了。
小结
梳理完了promise-polyfill源码的从 pending 状态到 fulfilled状态的执行流程。
- 在
new Promise的时候,传入的函数里面代码是同步执行的,resolve和reject方法是经过了防止多次调用的处理的,所以就算在方法里面调用多次也没有用。 then函数执行的时候,状态还没有变化时,只是将 传入函数 保存(_deferreds)起来,在之后状态变更后再遍历调用。- 当
resolve函数被执行,会修改当前promise状态,遍历执行_deferreds。并且会生成一个新的promise对象,后再去resolve这个新的对象,链式操作就是这样链接起来的。