最近在 github 上看到了一个 extremely lightweight Promise polyfill 实现,打开源码发现只有240行,果然极其轻量级,于是带着惊叹和好奇的心理去了解了下其具体实现。
源码的 github 地址: promise-polyfill
Promise 对于前端来说,是个老生常谈的话题,Promise 的出现解决了 js 回调地域的问题。目前市面上有很多 Promise 库,但其最终实现都要遵从 Promise/A+ 规范,这里对规范不做解读,有兴趣的可以查看链接内容。
Promise/A+规范链接
Promise/A+规范中文链接本篇文章将从 Promise 的使用角度来剖析源码具体实现。
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()函数实现有很大用处。
名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
_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 方法。
上面说到,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 方法来处理回调,支持无限链式调用,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
上面说了这么多,最终的 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 要么提供带返回值的回调,要么不提供回调函数
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 竞赛的机制,谁跑的快执行谁。
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源码注释版