Motivation
一个挺经典的前端面试题,自己polyfill实现Promise。在网友实现的基础上,自己理解加上注释。
这个问题涉及到了JavaScript作用域规则、事件循环、函数上下文、原型继承等诸多基础知识,理解完感觉很有收获,以注释形式记录下来。
关键思路
实现的关键思路,其实就是then调用有两种情况:
resolve生产value在then消费之前,即then时已经fulfilled:Promise保存了异步产生的value和reason,供then之后调用。
then消费在resolve生产之前,即then时还在pending:Promise push then的回调进队列,相当于注册回调。
所以本质上,Promise只是对之前注册回调方式的一种改写,变得更方便而已。
因为executor中resolve是下一个事件循环的(自己实现setTimeout,原生在微任务队列),所以new Promise().then(),then其实在resolve内部前执行,then注册的回调通过push进Promise对应的回调队列。
若也setTimeout调用then,resolve在then前,那保存resolve的value供之后调用即可。then时直接执行回调即可。
理解难点
整个实现的理解难点在于then方法。如何实现可链式调用的Promise。也就是then里面的resolvePromise(决议)要返回一个新的Promise。有一种衔尾蛇的感觉,十分烧脑。
简化理解就是,从resolvePromise的功能上理解:对then返回值再包装,返回一个新的Promise以供再调用。
这个再包装需要判断是否存在对同一个Promise的循环调用、是否是Promise(这个Promise是否已决议)、是否是thenable、是否多次调用等。
对于resolvePromise的细节仍然不太理解。比如针对thenable对象的called如何避免多次调用,什么情况下会有多次调用。
附上代码及注释:
/* * Promise 实现 遵循promise/A+规范 * 参考:https://www.jianshu.com/p/459a856c476f * Promise/A+规范译文: * https://malcolmyu.github.io/2015/06/12/Promises-A-Plus *//* * Promise/A+规范测试 * npm i -g promises-aplus-tests * promises-aplus-tests myPromise.js myPromise *//* * 基于Promise实现Deferred,promises-aplus-tests需要测试此方法 * Deferred和Promise的关系: * - Deferred 拥有 Promise * - Deferred 具备对 Promise的状态进行操作的特权方法(resolve reject) * *参考jQuery.Deferred *url: http://api.jquery.com/category/deferred-object/ */myPromise.deferred=function () { letdefer= {}; defer.promise=newmyPromise((resolve, reject) => { defer.resolve=resolve; defer.reject=reject; }); returndefer; }; try { module.exports=myPromise; } catch (e) {} // Promise内部状态机,三个状态,只能从PENDING到FULFILLED或者REJECTEDconstPENDING="pending"; constFULFILLED="fulfilled"; constREJECTED="rejected"; functionmyPromise(executor) { letthat=this; // 保存当前promise实例对象 // 若用class改写,可以避免这个thatthat.status=PENDING; // 初始状态that.value=undefined; // fulfilled状态时 返回的信息that.reason=undefined; // rejected状态时 拒绝的原因that.onFulfilledCallbacks= []; // 存储fulfilled状态对应的onFulfilled回调that.onRejectedCallbacks= []; // 存储rejected状态对应的onRejected回调functionresolve(value) { // value成功态时接收的终值if (valueinstanceofmyPromise) { returnvalue.then(resolve, reject); } // 为什么resolve 加setTimeout?// 2.2.4规范 onFulfilled 和 onRejected 只允许在 execution context 栈仅包含平台代码时运行.// 注1 这里的平台代码指的是引擎、环境以及 promise 的实施代码。// 也就是要确保 onFulfilled 和 onRejected 方法在下一轮事件循环执行(原生在微任务队列,还是本轮)。setTimeout(() => { // 调用resolve 回调对应onFulfilled函数if (that.status===PENDING) { // 只能由pedning状态 => fulfilled状态 (避免调用多次执行resolve reject回调)that.status=FULFILLED; that.value=value; that.onFulfilledCallbacks.forEach((cb) =>cb(that.value)); } }); } functionreject(reason) { // reason失败态时接收的拒因setTimeout(() => { // 调用reject 回调对应onRejected函数if (that.status===PENDING) { // 只能由pedning状态 => rejected状态 (避免调用多次resolve reject回调)that.status=REJECTED; that.reason=reason; that.onRejectedCallbacks.forEach((cb) =>cb(that.reason)); } }); } // 捕获在executor执行器中抛出的异常 也就是Promise中executor、then均有隐式的try catchtry { executor(resolve, reject); } catch (e) { reject(e); } } /** * resolve中的值几种情况: * 1.普通值 * 2.promise对象 * 3.thenable对象/函数 *//** * 对resolve 进行改造增强 针对resolve中不同值情况 进行处理 * @param {promise} promise2 promise1.then方法返回的新的promise对象 * @param {[type]} x promise1中onFulfilled的返回值 * @param {[type]} resolve promise2的resolve方法 * @param {[type]} reject promise2的reject方法 */functionresolvePromise(promise2, x, resolve, reject) { if (promise2===x) { // 如果从onFulfilled中返回的x 就是promise2 就会导致循环引用报错 这种情况见下面的用例,then的onfulfilled返回then返回的值,就会这样。returnreject(newTypeError("循环引用")); // 这样也能通过A+测试,但和原生的promise还是有差别。应该有默认的回调,rethrow出来。也就是说原生promise还是有无法polyfill的地方。 } letcalled=false; // 避免多次调用// 如果x是一个promise对象 (该判断和下面 判断是不是thenable对象重复 所以可有可无)if (xinstanceofmyPromise) { // 获得它的终值 继续resolveif (x.status===PENDING) { // 如果为等待态需等待直至 x 被执行或拒绝 并解析y值x.then( (y) => { resolvePromise(promise2, y, resolve, reject); }, (reason) => { reject(reason); } ); } else { // 如果 x 已经处于执行态/拒绝态(值已经被解析为普通值),用相同的值执行传递下去 promisex.then(resolve, reject); } // 如果 x 为对象或者函数 } elseif (x!=null&& (typeofx==="object"||typeofx==="function")) { try { // 是否是thenable对象(具有then方法的对象/函数)letthen=x.then; if (typeofthen==="function") { then.call( x, (y) => { if (called) return; called=true; resolvePromise(promise2, y, resolve, reject); }, (reason) => { if (called) return; called=true; reject(reason); } ); } else { // 说明是一个普通对象/函数resolve(x); } } catch (e) { if (called) return; called=true; reject(e); } } else { resolve(x); } } /** * @param {function} onFulfilled fulfilled状态时 执行的函数 * @param {function} onRejected rejected状态时 执行的函数 * @return {function} newPromsie 返回一个新的promise对象 */myPromise.prototype.then=function (onFulfilled, onRejected) { constthat=this; letpromise2; // 处理参数默认值 保证参数后续能够继续执行onFulfilled=typeofonFulfilled==="function"?onFulfilled : (value) =>value; onRejected=typeofonRejected==="function"?onRejected : (reason) => { throwreason; }; // then里面的FULFILLED/REJECTED状态时 为什么要加setTimeout ?// 原因:// 其一 2.2.4规范 要确保 onFulfilled 和 onRejected 方法异步执行(且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行) 所以要在resolve里加上setTimeout// 其二 2.2.6规范 对于一个promise,它的then方法可以调用多次.(当在其他程序中多次调用同一个promise的then时 由于之前状态已经为FULFILLED/REJECTED状态,则会走的下面逻辑),所以要确保为FULFILLED/REJECTED状态后 也要异步执行onFulfilled/onRejected// 其二 2.2.6规范 也是resolve函数里加setTimeout的原因// 总之都是 让then方法异步执行 也就是确保onFulfilled/onRejected异步执行// 如下面这种情景 多次调用p1.then// p1.then((value) => { // 此时p1.status 由pedding状态 => fulfilled状态// console.log(value); // resolve// // console.log(p1.status); // fulfilled// p1.then(value => { // 再次p1.then 这时已经为fulfilled状态 走的是fulfilled状态判断里的逻辑 所以我们也要确保判断里面onFuilled异步执行// console.log(value); // 'resolve'// });// console.log('当前执行栈中同步代码');// })// console.log('全局执行栈中同步代码');//if (that.status===FULFILLED) { // 成功态return (promise2=newmyPromise((resolve, reject) => { setTimeout(() => { try { letx=onFulfilled(that.value); resolvePromise(promise2, x, resolve, reject); // 新的promise resolve 上一个onFulfilled的返回值 } catch (e) { reject(e); // 捕获前面onFulfilled中抛出的异常 then(onFulfilled, onRejected); } }); })); } if (that.status===REJECTED) { // 失败态return (promise2=newmyPromise((resolve, reject) => { setTimeout(() => { try { letx=onRejected(that.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }); })); } if (that.status===PENDING) { // 等待态// 当异步调用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中// 一开始对用例1百思不得其解,为什么promise2会等于promise1???promise2刚new出来的,promise1是全局new的那个啊(理解错了,是then返回的而不是new返回的,因为对promise异步理解有误,习惯性以为then是之后才异步执行的)// 真相其实是!!!promise1、promise2都是地址,而他们指向的东西被改过。promise1被赋值的是then的范围值而不是new的对象。// 这里return ,new Promise时,其实只用管两个回调数组的push,push进去的回调函数在下一轮事件循环时才被调用(resolve被setTimeout了),而return,让外层的promise1实际上成promise2了。等到下一轮事件循环,push进去的回调函数执行时,promise1就是promise2.return (promise2=newmyPromise((resolve, reject) => { that.onFulfilledCallbacks.push((value) => { try { letx=onFulfilled(value); console.log(promise1===promise2); // 用例1:true,此处promise1和promise2指向同一处了resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }); that.onRejectedCallbacks.push((reason) => { try { letx=onRejected(reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }); })); } }; // 用于promise方法链时 捕获前面onFulfilled/onRejected抛出的异常myPromise.prototype.catch=function (onRejected) { returnthis.then(null, onRejected); // 其实和then没差别/* 但对以下的情况不同: * 1. new Promise(executor).then(onResolve, onReject) * 2. new Promise(executor).then(onResolve).catch(onReject) * 因为2会catch住onResolve中的错误,而1不会。 */}; myPromise.prototype.finally=function (callback) { // then里应该return一个Promise,不过没差,反正then会再包装。这样更好理解。returnthis.then( (value) => { callback(); returnvalue; }, (err) => { callback(); throwerr; } ); }; // 生成done函数,可以写在all里面的functiongen(length, resolve) { letcount=0; letvalues= []; returnfunction (i, value) { values[i] =value; if (++count===length) { console.log(values); resolve(values); } }; } /** * Promise.all Promise进行并行处理 * 参数: promise对象组成的数组作为参数 * 返回值: 返回一个Promise实例 * 当这个数组里的所有promise对象全部变为resolve状态的时候,才会resolve。 */myPromise.all=function (promises) { // 这个实现不是很严谨,比如对promises的类型校验returnnewmyPromise((resolve, reject) => { letdone=gen(promises.length, resolve); promises.forEach((promise, index) => { promise.then((value) => { done(index, value); }, reject); }); }); }; /** * Promise.allSettled Promise进行并行处理 * 参数: promise对象组成的数组作为参数 * 返回值: 返回一个Promise实例,value为每个promise的决议结果 * 一个promise失败不会导致所有失败 */// 实现1:myPromise.allSettled=function (promises) { returnmyPromise.all( promises.map((p) =>Promise.resolve(p).then( (res) => { return { status: "fulfilled", value: res }; }, (error) => { return { status: "rejected", reason: error }; } ) ) ); }; // 实现2Promise.allSettled=function (promises) { returnnewmyPromise((resolve) => { constdata= [], len=promises.length; letcount=len; for (leti=0; i<len; i+=1) { constpromise=promises[i]; promise .then( (res) => { data[i] = { status: "fulfilled", value: res }; }, (error) => { data[i] = { status: "rejected", reason: error }; } ) .finally(() => { // promise has been settledif (!--count) { resolve(data); } }); } }); }; /** * Promise.race * 参数: 接收 promise对象组成的数组作为参数 * 返回值: 返回一个Promise实例 * 只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理(取决于哪一个更快) */myPromise.race=function (promises) { returnnewmyPromise((resolve, reject) => { promises.forEach((promise, index) => { promise.then(resolve, reject); }); }); }; myPromise.resolve=function (value) { returnnewmyPromise((resolve) => { resolve(value); }); }; myPromise.reject=function (reason) { returnnewmyPromise((resolve, reject) => { reject(reason); }); }; // 测试用例debugger; // 用例1,对应promise1 = promise2,循环调用的情况。varpromise1=newmyPromise((resolve, reject) => { // 注意,若用let,得不到1、2的效果,因为在初始化前访问promise1会”Cannot access 'promise1' before initialization“// 也就是let的初始化前的“暂时性死区”,而var的话,只会undefinedconsole.log("resolve的promise1", promise1); // 1.此处promise1还是undefinedresolve(promise1); }).then((result) => { console.log("then里的result === promise1", result===promise1); console.log("then里promise1", promise1); returnpromise1; // 2.此处的promise1已经是执行完构造函数的一个myPromise 思考1、2,其实是JavaScript作用域链的细节,1处resolve进去的只是个变量名(一个框,一个容器、一个地址)而已,then时执行的时候再按这个变量名查找,那时promise1这个“框”里已经有东西了}); // 用例2,原生Promise可以catch回已经报出的错误。letb=newPromise((resolve, reject) => { resolve(); }).then(() => { returnb; }); // 此处对比可看出,原生promise rethrow了没catch住的error,但自己实现的A+测试没有。可以rethrow,但做不到“收回”letc=newPromise((resolve, reject) => { reject(); }); // 此处对比可看出,原生promise rethrow了没catch住的error,但自己实现的A+测试没有setTimeout(() => { c.catch(() => {}); }, 1000); // 此处可以看出,先log出的error被“收回”了,这是自己实现的promise没法做到的 setTimeout 0ms也是一样的,因为都在下一个事件循环。此处只是为了看出效果。// 用例3,用于观察resolvePromise的逻辑。letd=newmyPromise((resolve, reject) => { resolve(); }).then(() => { returnnewmyPromise(() => {}); // 这个用例用于观察then返回一个不会fulfill的状态时,resolvePromise内怎么做});
参考资料
- 代码实现:https://www.jianshu.com/p/459a856c476f
- Promise/A+规范译文:https://malcolmyu.github.io/2015/06/12/Promises-A-Plus
- Promise/A+规范测试
npm i -g promises-aplus-tests promises-aplus-tests myPromise.js myPromise
- promises-aplus-tests测试要求实现的deferred方法:http://api.jquery.com/category/deferred-object/