从使用角度渐进式剖析Promise源码
开篇
最近在 github 上看到了一个 extremely lightweight Promise polyfill 实现,打开源码发现只有240行,果然极其轻量级,于是带着惊叹和好奇的心理去了解了下其具体实现。
源码的 github 地址:promise-polyfill
Promise 对于前端来说,是个老生常谈的话题,Promise 的出现解决了 js 回调地域的问题。目前市面上有很多 Promise 库,但其最终实现都要遵从 Promise/A+ 规范,这里对规范不做解读,有兴趣的可以查看链接内容。
Promise/A+规范链接
Promise/A+规范中文链接
本篇文章将从 Promise 的使用角度来剖析源码具体实现。
API 列表
Promise // 构造函数 Promise.prototype.then Promise.prototype.catch Promise.prototype.finally // 静态方法 Promise.resolve Promise.reject Promise.race Promise.all
源码解析
构造函数
使用
Promise 使用第一步,构造实例,传入 Function 形参,形参接收两个 Function 类型参数resolve, reject
const asyncTask = () => {}; const pro = new Promise((resolve, reject) => { asyncTask((err, data) => { if (err) { reject(err); } else { resolve(data); } }); });
源码
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'); this._state = 0; this._handled = false; this._value = undefined; this._deferreds = []; doResolve(fn, this); } function doResolve(fn, self) { // done变量保护 resolve 和 reject 只执行一次 // 这个done在 Promise.race()函数中有用 var done = false; try { // 立即执行 Promise 传入的 fn(resolve,reject) fn( function(value) { // resolve 回调 if (done) return; done = true; resolve(self, value); }, function(reason) { // reject 回调 if (done) return; done = true; reject(self, reason); } ); } catch (ex) { if (done) return; done = true; reject(self, ex); } }
Promise必须通过构造函数实例化来使用,传入 Promise 构造函数的形参 fn 在doResolve方法内是 立即调用执行 的,并没有异步(指放入事件循环队列)处理。doResolve内部针对 fn 函数的回调参数做了封装处理,done变量保证了 resolve reject 方法只执行一次,这在后面说到的Promise.race()函数实现有很大用处。
Promise 实例的内部变量介绍
名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
_state | Number | 0 | Promise内部状态码 |
_handled | Boolean | false | onFulfilled,onRejected是否被处理过 |
_value | Any | undefined | Promise 内部值,resolve 或者 reject返回的值 |
_deferreds | Array | [] | 存放 Handle 实例对象的数组,缓存 then 方法传入的回调 |
_state枚举值类型
_state === 0 // pending _state === 1 // fulfilled,执行了resolve函数,并且_value instanceof Promise === true _state === 2 // rejected,执行了reject函数 _state === 3 // fulfilled,执行了resolve函数,并且_value instanceof Promise === false
注意:这里_state区分了1 和 3 两种状态,下面会解释原因
/** * Handle 构造函数 * @param onFulfilled resolve 回调函数 * @param onRejected reject 回调函数 * @param promise 下一个 promise 实例对象 * @constructor */ function Handler(onFulfilled, onRejected, promise) { this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null; this.onRejected = typeof onRejected === 'function' ? onRejected : null; this.promise = promise; }
_deferreds数组的意义:当在 Promise 内部调用了异步处理任务时,pro.then(onFulfilled,onRejected)传入的两个函数不会立即执行,所以此时会把当前的回调和下一个 pro 对象关联缓存起来,待到 resolve 或者 reject触发调用时,会去 forEach 这个_deferreds数组中的每个 Handle 实例去处理对应的 onFulfilled,onRejected 方法。
Promise 内部 resolve reject finale 方法
上面说到,doResolve 内部做了 fn 的立即执行,并保证 resolve 和 reject 方法只执行一次,接下来说说resolve 和 reject 内部具体做了什么
function resolve(self, newValue) { try { // resolve 的值不能为本身 this 对象 // 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.'); // 针对 resolve 值为 Promise 对象的情况处理 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') { // 兼容类 Promise 对象的处理方式,对其 then 方法继续执行 doResolve doResolve(bind(then, newValue), self); return; } } // resolve 正常值的流程,_state = 1 self._state = 1; self._value = newValue; finale(self); } catch (e) { reject(self, e); } } function reject(self, newValue) { self._state = 2; self._value = newValue; finale(self); } function finale(self) { // Promise reject 情况,但是 then 方法未提供 reject 回调函数参数 或者 未实现 catch 函数 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++) { // 这里调用之前 then 方法传入的onFulfilled, onRejected函数 // self._deferreds[i] => Handler 实例对象 handle(self, self._deferreds[i]); } self._deferreds = null; }
resolve,reject 是由用户在异步任务里面触发的回调函数
调用 resolve reject 方法的注意点
1、newValue不能为当前的 this 对象,即下面的这样写法是错误的
const pro = new Promise((resolve)=>{setTimeout(function () { resolve(pro); },1000)}); pro.then(data => console.log(data)).catch(err => {console.log(err)});
因为resolve做了 try catch 的操作,直接会进入 reject 流程。
2、newValue可以为另一个Promise 对象类型实例, resolve 的值返回的是另一个 Promise 对象实例的内部的_value,而不是其本身 Promise 对象。即可以这样写
const pro1 = new Promise((resolve)=>{setTimeout(function () { resolve(100); },2000)}); const pro = new Promise((resolve)=>{setTimeout(function () { resolve(pro1); },1000)}); pro.then(data => console.log('resolve' + data)).catch(err => {console.log('reject' + err)}); // 输出结果:resolve 100 // data 并不是pro1对象
具体原因就在 resolve 方法体内部做了newValue instanceof Promise的判断,并将当前的_state=3,self._value = newValue,然后进入 finale 方法体,在 handle 方法做了核心处理,这个下面介绍 handle 方法会说到;
这里有一个注意点,resolve 的 value 可能是其他框架的 Promise(比如:global.Promise,nodejs 内部的 Promise 实现) 构造实例,所以在typeof then === 'function'条件下做了doResolve(bind(then, newValue), self);的重新调用,继续执行当前类型的 Promise then 方法,即又重新回到了doResolve流程。
如果这里的实现方式稍微调整下,即不管newValue是自身的 Promise 实例还是其他框架实现的 Promise实例,都执行doResolve(bind(then, newValue), self)也能行得通,只不过会多执行 then 方式一次,从代码性能上说,上面的实现方式会更好。参照代码如下
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') ) { // 这里简单粗暴处理,无论是 Promise 还是 global.Promise // 都直接调用doResolve var then = newValue.then; if (typeof then === 'function') { doResolve(bind(then, newValue), self); return; } } // resolve 正常值的流程,_state = 1 self._state = 1; self._value = newValue; finale(self); } catch (e) { reject(self, e); } }
所有 resolve 和 reject 的值最终都会去到finale函数中去处理,只不过在这里的_state状态会有所不同;当Promise 出现reject的情况时,而没有提供 onRejected 函数时,内部会打印一个错误出来,提示要捕获错误。代码实现即
const pro = new Promise((resolve,reject)=>{setTimeout(function () { reject(100); },1000)}); pro.then(data => console.log(data)); // 会报错 pro.then(data => console.log(data)).catch(); // 会报错 pro.then(data => console.log(data)).catch(()=>{}); // 不会报错 pro.then(data => console.log(data),()=>{}) // 不会报错
then、catch、finally 方法
第二步,调用 then 方法来处理回调,支持无限链式调用,then 方法第一个参数成功回调,第二个参数失败或者异常回调
源码
function noop() {} Promise.prototype.then = function(onFulfilled, onRejected) { var prom = new this.constructor(noop); handle(this, new Handler(onFulfilled, onRejected, prom)); return prom; }; Promise.prototype['catch'] = function(onRejected) { return this.then(null, onRejected); }; Promise.prototype['finally'] = function(callback) { var constructor = this.constructor; return this.then( function(value) { return constructor.resolve(callback()).then(function() { return value; }); }, function(reason) { return constructor.resolve(callback()).then(function() { return constructor.reject(reason); }); } ); };
Promise.prototype.then方法内部构造了一个新的Promsie 实例并返回,这样从 api 角度解决了 Promise 链式调用的问题,而且值得注意的是,每个 then 方法返回的都是一个新的 Promise 对象,并不是当前的 this链接调用方式。最终的处理都会调用 handle 方法。
catch方法在 then 方法上做了一个简单的封装,所以从这里也可以看出,then 方法的形参并不是必传的,catch 只接收onRejected。
finally方法不管是调用了 then 还是 catch,最终都会执行到finally的 callback
核心逻辑:handle方法内部实现
上面说了这么多,最终的 resolve reject 回调处理都会进入到 handle 方法中,来处理onFulfilled 和 onRejected,先看源码
Promise._immediateFn = (typeof setImmediate === 'function' && function(fn) { setImmediate(fn); }) || function(fn) { setTimeoutFunc(fn, 0); }; function handle(self, deferred) { // 如果当前的self._value instanceof Promise // 将self._value => self,接下来处理新 Promise while (self._state === 3) { self = self._value; } // self._state=== 0 说明还没有执行 resolve || reject 方法 // 此处将 handle 挂起 if (self._state === 0) { self._deferreds.push(deferred); return; } self._handled = true; // 通过事件循环异步来做回调的处理 Promise._immediateFn(function() { // deferred.promise :第一个 Promise then 方法 返回的新 Promise 对象 // 这里调用下一个 Promise 对象的 then 方法的回调函数 // 如果当前 Promise resolve 了,则调用下一个 Promise 的 resolve方法,反之,则调用下一个 Promise 的 reject 回调 // 如果当前 Promise resolve 了,则调用下一个 Promise 的 resolve方法 // cb回调方法:如果自己有onFulfilled||onRejected方法,则执行自己的方法;如果没有,则调用下一个 Promise 对象的onFulfilled||onRejected var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected; // 自己没有回调函数,进入下一个 Promise 对象的回调 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; } // 处理下一个 Promise 的 then 回调方法 // ret 作为上一个Promise then 回调 return的值 => 返回给下一个Promise then 作为输入值 resolve(deferred.promise, ret); }); }
self._state === 3,说明当前 resolve(promise)方法回传的值类型为 Promise 对象,
即 self._value instanceOf Promise === true, 将 self=self._value,即当前处理变更到了新的 Promise 对象上 ,如果当前 promise对象内部状态是fulfilled或者 rejected,则直接处理onFulfilled 或者 onRejected回调;如果仍然是 padding 状态,则继续等待。这就很好的解释了为什么resolve(pro1),pro.then的回调取的值却是 pro1._value.
从使用角度来看
const pro1 = new Promise(resolve=>{setTimeout(()=>{resolve(100)},1000)}) // 执行耗时1s 的异步任务 pro.then(()=>pro1).then(data => console.log(data)).catch(err => {}); // 输出结果: 正常打印了100,data并不是当前的pro1对象
pro1内部是耗时1s 的异步任务,此时self._state === 0,即内部是 Padding 状态,则将deferred对象 push 到_deferreds数组里面,然后等待 pro1内部调用resolve(100)时,继续上面resolve方法体执行
const pro1 = new Promise(resolve=>resolve(100)}) // 执行同步任务 pro.then(()=>pro1).then(data => console.log(data)).catch(err => {}); // 输出结果: 正常打印了100,data并不是当前的pro1对象
但是如果pro1内部是同步任务,立即执行的话,当前的self._state === 1,即调过 push 到_deferreds数组的操作,执行最后的onFulfilled, onRejected回调,onFulfilled, onRejected会被放入到事件循环队列里面执行,即执行到了Promise._immediateFn
Promise._immediateFn回调函数放到了事件循环队列里面来执行
这里的deferred对象存放了当前的onFulfilled和onRejected回调函数和下一个 promise 对象。
当前对象的onFulfilled和onRejected如果存在时,则执行自己的回调;
pro.then(data => data}).then(data => data).catch(err => {}); // 正确写法: 输出两次 data
注意:then 方法一定要做 return 下一个值的操作,因为当前的 ret 值会被带入到下一个 Promise 对象,即 resolve(deferred.promise, ret)。如果不提供返回值,则第二个 then 的 data 会变成 undefined,即这样的错误写法
pro.then(data => {}}).then(data => data).catch(err => {}); // 错误写法: 第二个 then 方法的 data 为 undefined
如果onFulfilled和onRejected回调不存在,则执行下一个 promise 的回调并携带当前的_value 值。即可以这样写
pro.then().then().then().then(data => {}).catch(err => {}); // 正确写法: 第四个 then 方法仍然能取到第一个pro 的内部_value 值 // 当然前面的三个 then 写起来毫无用处
所以针对下面的情况:当第一个 then 提供了 reject 回调,后面又跟了个 catch 方法。
当 reject 时,会优先执行第一个 Promise 的onRejected回调函数,catch 是在下一个 Promise 对象上的捕获错误方法
pro.then(data => data,err => err).catch(err => err);
最终总结:resolve 要么提供带返回值的回调,要么不提供回调函数
静态方法:race
Promise.race = function(values) { return new Promise(function(resolve, reject) { for (var i = 0, len = values.length; i < len; i++) { // 因为doResolve方法内部 done 变量控制了对 resolve reject 方法只执行一次的处理 // 所以这里实现很简单,清晰明了,最快的 Promise 执行了 resolve||reject,后面相对慢的 // Promise都不执行 values[i].then(resolve, reject); } }); };
用法
Promise.race([pro1,pro2,pro3]).then()
race的实现非常巧妙,对当前的 values(必须是 Promise 数组) for 循环执行每个 Promise 的 then 方法,resolve, reject方法对于所有race中 promise 对象都是公用的,从而利用doResolve内部的 done变量,保证了最快执行的 Promise 能做 resolve reject 的回调,从而达到了多个Promise race 竞赛的机制,谁跑的快执行谁。
静态方法:all
Promise.all = function(arr) { return new Promise(function(resolve, reject) { if (!arr || typeof arr.length === 'undefined') throw new TypeError('Promise.all accepts an array'); var args = Array.prototype.slice.call(arr); if (args.length === 0) return resolve([]); var remaining = args.length; function res(i, val) { try { // 如果 val 是 Promise 对象的话,则执行 Promise,直到 resolve 了一个非 Promise 对象 if (val && (typeof val === 'object' || typeof val === 'function')) { var then = val.then; if (typeof then === 'function') { then.call( val, function(val) { res(i, val); }, reject ); return; } } // 用当前resolve||reject 的值重写 args[i]{Promise} 对象 args[i] = val; // 直到所有的 Promise 都执行完毕,则 resolve all 的 Promise 对象,返回args数组结果 if (--remaining === 0) { resolve(args); } } catch (ex) { // 只要其中一个 Promise 出现异常,则全部的 Promise 执行退出,进入 catch异常处理 // 因为 resolve 和 reject 回调有 done 变量的保证只能执行一次,所以其他的 Promise 都不执行 reject(ex); } } for (var i = 0; i < args.length; i++) { res(i, args[i]); } }); };
用法
Promise.all([pro1,pro2,pro3]).then()
all 等待所有的 Promise 都执行完毕,才会执行 Promise.all().then()回调,只要其中一个出错,则直接进入错误回调,因为对于所有 all 中 promise 对象 reject 回调是公用的,利用doResolve内部的 done变量,保证一次错误终止所有操作。
但是对于 resolve 则不一样, resolve 回调函数通过 res 递归调用自己,从而保证其值_value不为 Promise 类型才结束,并将_value 赋值到 args 数组,最后直到所有的数组Promise都处理完毕由统一的 resolve 方法结束当前的 all 操作,进入 then 处理流程。
结束语
本篇针对 Promise 的所有 api 做了详细的代码解释和使用场景,篇幅可能过长,看起来比较费力,如果有写的不对的地方欢迎指正。
最后附上我的 github 源码注释版链接 promise源码注释版