通过polyfill理解Promise原理

简介: 一个挺经典的前端面试题,自己polyfill实现Promise。在网友实现的基础上,自己理解加上注释。这个问题涉及到了JavaScript作用域规则、事件循环、函数上下文、原型继承等诸多基础知识,理解完感觉很有收获,以注释形式记录下来。

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内怎么做});


参考资料


  1. 代码实现:https://www.jianshu.com/p/459a856c476f
  2. Promise/A+规范译文:https://malcolmyu.github.io/2015/06/12/Promises-A-Plus
  3. Promise/A+规范测试
npm i -g promises-aplus-tests
promises-aplus-tests myPromise.js myPromise
  1. promises-aplus-tests测试要求实现的deferred方法:http://api.jquery.com/category/deferred-object/
相关文章
|
6月前
|
前端开发
Promise的用法&原理&手写实现-2
Promise的用法&原理&手写实现-2
21 1
|
3月前
|
前端开发 JavaScript API
Promise.all() 的原理与实战:简化异步逻辑的不二选择
Promise.all() 的原理与实战:简化异步逻辑的不二选择
Promise.all() 的原理与实战:简化异步逻辑的不二选择
|
6月前
|
前端开发 JavaScript API
Promise的用法&原理&手写实现-1
Promise的用法&原理&手写实现-1
23 0
|
11月前
|
存储 缓存 自然语言处理
吊打面试官:promise原理详解
吊打面试官:promise原理详解
157 0
|
前端开发
重新手写promise,理解核心的异步链式调用原理
重新手写promise,理解核心的异步链式调用原理
147 0
|
前端开发 JavaScript 测试技术
Promise.race() 原理解析及使用指南
Promise 对象是 ECMAScript 6 中新增的对象,主要将 JavaScript 中的异步处理对象和处理规则进行了规范化。前面介绍了《Promise.any() 原理解析及使用指南》、《Promise.all() 原理解析及使用指南》和《Promise.allSettled() 原理解析及使用指南》
314 0
Promise.race() 原理解析及使用指南
|
前端开发 JavaScript
Promise.allSettled() 原理解析及使用指南
Promise 对象是ECMAScript 6中新增的对象,主要将 JavaScript 中的异步处理对象和处理规则进行了规范化。前面介绍了《Promise.any() 原理解析及使用指南》和《Promise.all() 原理解析及使用指南》,本文继续来介绍 Promise 另一个方法 Promise.allSettled(promises) ,返回一个在所有给定的 promise 都已经 fulfilled 或 rejected 后的 promise ,并带有一个对象数组,每个对象表示对应的promise 结果。
210 0
Promise.allSettled() 原理解析及使用指南
|
前端开发 JavaScript
Promise.all() 原理解析及使用指南
Promise 对象是ECMAScript 6中新增的对象,主要将 JavaScript 中的异步处理对象和处理规则进行了规范化。前面介绍了《Promise.any() 原理解析及使用指南》,本文来介绍另一个方法 Promise.all(promises) ,能够一次并行处理多个 promise,并且只返回一个 promise 实例, 那个输入的所有 promise 的 resolve 回调的结果是一个数组。
597 0
Promise.all() 原理解析及使用指南
|
前端开发
Promise.any() 原理解析及使用指南
Promise.any(promises) 是 ES2021 新增的特性,它能够并行运行 promise,并解析为 promises 列表中第一个成功解析的 promise 的值。需要注意的是 Promise.any() 方法依然是实验性的,尚未被所有的浏览器完全支持。
398 0
Promise.any() 原理解析及使用指南
|
设计模式 前端开发 JavaScript
图解JavaScript——代码实现【2】(重点是Promise、Async、发布/订阅原理实现)
图解JavaScript——代码实现【2】(重点是Promise、Async、发布/订阅原理实现)
图解JavaScript——代码实现【2】(重点是Promise、Async、发布/订阅原理实现)