作者:一阵风,一枚只想安静写代码的程序员,来自程序员成长指北交流群    github: https://github.com/yizhengfeng-jj/promise

前言

promise 是 js 里面非常重要的一部分,搞懂了 promise 才能更好的去理解 async,await 和 generator。但是往往很多时候就是不理解 promise 的机制,所以这次通过一步步实现一个 promise 来加深自己的印象,提高自己的思维。

大概的架子

通过我们经常写的 promise 语法,我们可以先写一个大概的架子出来,promise 接受回调,并且调用,自身带有三种状态,pendding, onFulfilled, onRejected,并且 resolve 这个函数可以让 pendding 状态变成 onFulfilled 状态,同理 reject 函数可以让 pendding 状态变成 onRejected 状态。我们先把上面描述部分实现了。

const PromiseCopy = function (fn) {this.info = {status: "pending",value: "",};const self = this;self.onFulfilledArr = []; // then函数里面的第一个回调函数的集合self.onRejectedArr = []; // then函数里面的第二个回调函数的集合const resolve = function (value) {// 加这个判断是为了表示,只有在pendding状态下才会去执行// 状态已经变成onFulfilled之后就不能再去改变了// 符合PromiseA+中的2.1.2.1if (self.info.status === "pending") {self.info.status = "onFulfilled";self.info.value = value;self.onFulfilledArr.forEach((fn) => fn(value));}};// 和上面同理符合PromiseA+,2.1.3.1const reject = function (value) {if (self.info.status === "pending") {self.info.status = "onRejected";self.info.value = value;self.onRejectedArr.forEach((fn) => fn(value));}};fn(resolve, reject);
};

resolve 的附加实现

其实写到这里我们的 resolve 函数还是有一些功能没有实现的, 我们知道 调用 resolve(x), x 的值有好几种情况,如下

  • 如果 x 是 Promise 实例本身,则抛出错误

  • 如果 x 是一个 Promise 对象,那么 then 函数的执行取决这个 x 的状态,如果 x 也调用 resolve(y),其中 y 也是一个 promise 对象.那么 then 函数的执行取决于这个 promise 对象,依次类推,直到最后一个 promise 状态更改

  • 如果 x 是一个 thenable 对象,就是一个对象包含 then 这个属性,或者是一个函数包含一个 then 的静态方法,那么直接执行 then 函数

  • 如果 x 是一个普通值,直接变成 onFulfilled 状态,执行后面的 then 函数

思考

  • 网上实现的大部分 resolve 都是我上面的代码,但是根据规范,resolve 函数里面应该是要判断上面几点的,所以我们上面写的代码是有误的

  • 还有一个问题是,我们需要在 resolve 函数里面判断 x 是不是实例的本身,但是正常的 resolve 函数我们经常是传入一个参数,所以中间肯定是有一个中间函数的,看下面的代码

const PromiseCopy = function (fn) {this.info = {status: "pending",value: "",};const self = this;self.onFulfilledArr = []; // then函数里面的第一个回调函数的集合self.onRejectedArr = []; // then函数里面的第二个回调函数的集合// _resolve 是我们经常调用的resolve// 但是真正实现的应该是里面的resolveconst _resolve = function (value) {// 这个函数得改变一下// PromiseCopy一旦被实例化,那么self就是实例本身了resolve(self, value);};// 此时我们就可以在resolve进行判断了const resolve = function (promise, value) {let ifexec = false;// 首先判断value是不是promise本身if (value === promise) {// 一定要用TypeError写 不然promises-aplus-tests跑不通// 切记这是第一个坑,promises-aplus-tests只认TypeError这种错误形式reject(new TypeError("A promise cannot be onFulfilled with itself."));}// value是一个thenable对象// 这个要Object.prototype.toString.call(value) === "[object Object]"判断// 不然resolve([])有问题,不知道是不是我实现问题if (value &&(Object.prototype.toString.call(value) === "[object Object]" ||typeof value === "function")) {// var promise1 = Promise.resolve(dump).then(function () {//   return {//     then: (resolve, reject) => {//       setTimeout(() => {//         resolve({//           then: (resolve, reject) => {//             resolve("aa111a");//             throw "other";//           },//         });//       });//     },//   };// });// promise1.then(//   (res) => {//     console.log(res === "aa111a");//     console.log("aaa");//   },//   (res) => {//     console.log(res);//     console.log("error");//   }// );// 这里的try--catch一定要加 ,不然会promises-aplus-tests会一直报错,这是第三个大坑// 因为promises-aplus-test测试里面有这一条的// 看上面注释例子try {// 拿到then函数const then = value.then;// 如果then是一个函数则执行这个函数if (typeof then === "function") {// 为什么要.call(value, x, y) 你们可以自己试一下原生的Promise在这种情况下this指向的就是value,所以要绑定// 因为then我们已经拿出来了then = value.then,直接调用then(),this就指向的window// 为什么后面还需要绑定两个函数了// 根据原生的Promise可知,thenable中的then函数可以接受两个函数resolve,reject// 只有手动调用了resolve和reject才会执行后面的.then操作,具体大家自己操作下then.call(value,function (value) {if (ifexec) {return;}// ifexec这个一定要加,不然也会报200ms错误,第四个大坑// 目的是为了不让多次执行,语言无法表达看下面的例子// var promise1 = Promise.resolve(dump).then(function () {//   return {//     then: (resolve, reject) => {//       resolve("aa111a");//       resolve("aa111a");//     },//   };// });ifexec = true;resolve(promise, value);},function (value) {if (ifexec) {return;}ifexec = true;reject(value);});return;}} catch (e) {if (ifexec) {return;}ifexec = true;reject(e);}}// 下面这一点非常的重要,是async,await 和一些插件比如saga的核心// 就是如果x是一个promise对象,那么then的执行取决于x的状态// 还有这一个判断一定要放在这里,不要和上面的换 不然promises-aplus-tests会报一个超过200ms的错误,切记这是第二个坑if (value && value instanceof PromiseCopy && value.then === promise.then) {// 将promise的onFulfilledArr给到value// 但是还没有那么简单我们要明白两点// 如果value这个promise已经不是pendding,我们给了他也没有用,所以需要直接调用if (value.info.status === "pending") {value.onFulfilledArr = self.onFulfilledArr;value.onRejectedArr = self.onRejectedArr;}// 如果value状态是onFulfilledif (value.info.status === "onRejected") {self.info.value = value.info.value;self.onRejectedArr.forEach((fn) => fn(value.info.value));}// 如果value状态是rejectif (value.info.status === "onFulfilled") {self.info.value = value.info.value;self.onFulfilledArr.forEach((fn) => fn(value.info.value));}return;}// 如果是一个普通的值// 加这个判断是为了表示,只有在pendding状态下才会去执行// 状态已经变成onFulfilled之后就不能再去改变了// 符合PromiseA+中的2.1.2.1if (self.info.status === "pending") {self.info.status = "onFulfilled";self.info.value = value;self.onFulfilledArr.forEach((fn) => fn(value));}};// 和上面同理符合PromiseA+,2.1.3.1// reject没有resolve那么多规则,比较简单const reject = function (value) {if (self.info.status === "pending") {self.info.status = "onRejected";self.info.value = value;self.onRejectedArr.forEach((fn) => fn(value));}};// 此时fn调用的是_reoslve// 这个try catch主要是实现promiseCopy.prototype.catchtry {fn(_resolve, reject);} catch (e) {setTimeout(() => {self.onRejectedArr.forEach((fn) => fn(e));});}
};

then 的实现

我们上面介绍的是 promise 的 resolve 用法,promise 还有一个基本用法就是后面接 then,因为是.then 所以我们想到的是这个 then 方法挂在到原型上的,那么 new PromiseCopy 的时候就可以得到这个 then。then 里面是两个函数,一个是 onFulfilled 后执行的回调,一个是 onRejected 后执行的回调。现在的问题是他是怎么做到 then 里面的函数是在 resolve 和 reject 后执行的?这种推迟执行或者说在某种情况下去执行我们想到的就是观察者模式了。下面用代码把上面的话实现一遍,在代码里面会写详细一点的注释。

PromiseCopy.prototype.then = function (onFulfilled, onRejected) {const self = this;// 这里要判断下,如果PromiseCopy是resolve了那么就直接执行onFulfilledif (self.info.status === "onFulfilled") {setTimeout(() => {onFulfilled(self.info.value);});}if (self.info.status === "onRejected") {setTimeout(() => {onRejected(self.info.value);});}// 根据PromiseA+中的2.2.1.1和2.2.1.2,onFulfilled和onRejected必须是函数,不然就会被忽略if (typeof onFulfilled === "function") {self.onFulfilledArr.push(() => {setTimeout(() => {onFulfilled(self.info.value);});});}if (typeof onRejected === "function") {self.onRejectedArr.push(() => {setTimeout(() => {onRejected(self.info.value);});});}// 根据PromiseA+ 2.2.7规范 then函数必须返回一个promise对象return new PromiseCopy((resolve, reject) => {});
};

then 的额外实现

上面实现的 then 也是一个简单的用法,不过根据 PromiseA+的规范这个 then 函数还有几个点没有实现,看代码解释

promise2 = promise1.then(onFulfilled, onRejected);promise2.then(onFulfilled, onRejected);
  • promise1.then 中的 onFulfilled,onRejected 函数如果返回一个 x,那么当作[[Resolve]](promise2, x)来处理,就跟上面的 resolve 一样处理,注意如果函数什么都没有返回,就是返回的 undefined

  • promise1.then 函数中的两个回调函数只要有一个报错,那么直接调用 promise2.then 函数中的错误回调

  • 如果 promise1.then 的第一个回调不是函数,并且 promise1 调用的是 resolve,那么 promise2.then 的第一个回调参数是 promise1 中 resolve 函数的抛出值

  • 同理,如果 promise1.then 第二个回调不是函数,并且 promise1 调用的是 reject,那么 promise2.then 中的错误回调就会执行

思考

如果像上面这么说的话,这个新抛出来的 promise 何时调用这个 resolve 或者 reject 是一个关键, 并且这个抛出的 promise 的执行还得看 onFulfilled 和 onRejected 返回值,这一点当时写 promise 的时候想了很久,不知道如何组织,后来实在想不出来,看了下网上很多文章,发现这些逻辑都是在 PromiseCopy 主体里面实现的。

return new PromiseCopy((resolve, reject) => {});

then 实现加强版

PromiseCopy.prototype.then = function (onFulfilled, onRejected) {const self = this;// 这个一定要这么写目的为了让值传递onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (val) => val;// 这个一定要这么写,一定要抛出一个错throw erronRejected =typeof onRejected === "function"? onRejected: (err) => {throw err;};const newPromise = new PromiseCopy((resolve, reject) => {if (self.info.status === "onFulfilled") {setTimeout(() => {try {// 如果onFulfilled不是一个函数resolve--self.info.valuelet value = self.info.value;// 这个注释不要,留着只是为了记录当时的思路// 这个加判断是为了防止then函数逇回调不是一个函数,,是一个字符串//   if (typeof onFulfilled === "function") {//     value = onFulfilled(value);//   }value = onFulfilled(value);// 这里要做一个[[Resolve]](promise2, x)处理了// 因为resolve里面直接做了,所以直接调用,和网上的一些实现有点不一样// 他们是提取了一个resolvePromise函数调用,我是直接调用了resolveresolve(value);} catch (e) {reject(e);}});}// 注意这里根据上面可知onFulfilled,onRejected抛出的值都要经过[[Resolve]](promise2, x)// 这和resolve,reject不一样,promise中resolve才走[[Resolve]](promise2, x),reject不走if (self.info.status === "onRejected") {setTimeout(() => {try {let { value } = self.info;value = onRejected(self.info.value);resolve(value);} catch (e) {reject(e);}});}// 如果是pending状态也需要pushif (self.info.status === "pending") {self.onFulfilledArr.push((data) => {setTimeout(() => {try {let value = data;value = onFulfilled(value);resolve(value);} catch (e) {reject(e);}});});self.onRejectedArr.push((data) => {setTimeout(() => {try {let value = data;value = onRejected(data);resolve(value);} catch (e) {reject(e);}});});}});return newPromise;
};

小结

到这里 promise 的主体实现已经完成了,下面是测试结果

Promise 其他静态方法

Promise.resolve

PromiseCopy.resolve = function (data) {return new PromiseCopy((resolve, reject) => {resolve(data);});
};

reject

Promise.reject = function (reason) {return new Promise((resolve, reject) => {reject(reason);});
};

Promise.all

这个方法有几个特点如下

  • 该方法接受一个数组,数组每一个元素都是一个 promise 对象

  • 只有所有 promise 都是 onFulfilled 的时候才会执行 then 回调,并且结果顺序和数组的一致

  • 如果其中一个 promise 发生了 reject 那么就会返回这个值

PromiseCopy.all = function (data) {let count = 0; // 记录调用次数let total = data.length;let result = [];return new PromiseCopy((resolve, reject) => {for (let i = 0; i < total; i++) {data[i].then((res) => {result.push(res);++count;if (count === totla) {resolve(result);}},(res) => {return reject(res);});}});
};

Promise.race

这个方法也有以下几个特点

  • 这个方法也是接受数组,数组的元素是 promise

  • 他只返回最快的那一个 promise 的值

  • 就算有错误也会返回最快那一个 promise 的值

PromiseCopy.race = function (data) {const total = data.length;return new PromiseCopy((resolve, reject) => {for (let i = 0; i < total; i++) {data[i].then((res) => {resolve(res);},(res) => {return reject(res);});}});
};

catch 方法

PromiseCopy.prototype.catch = function (onRejected) {// 能到catch里面来的一定是走的reject的// 而且状态一定是penddingconst self = this;const newPromise = new PromiseCopy((resolve, reject) => {if (self.info.status === "onRejected") {try {setTimeout(() => {let { value } = self.info;if (typeof onRejected === "function") {value = onRejected(self.info.value);}resolve(value);});} catch (e) {rejetc(e);}}if (self.info.status === "pending") {self.onRejectedArr.push((data) => {setTimeout(() => {try {let value = data;if (typeof onRejected === "function") {value = onRejected(data);}resolve(value);} catch (e) {reject(e);}});});}});return newPromise;
};// 后来发现catch有一个简单的实现方法
// 没有删除上面就是为了记录思路过程
Promise.prototype.catch = function (onRejected) {return this.then(null, onRejected);
};

deferred

这个是 Promise 提供的一个快捷使用,自己实现 promise 的时候一定要加,不然 promises-aplus-tests promise.js 跑不过

PromiseCopy.defer = PromiseCopy.deferred = function () {let dfd = {};dfd.promise = new PromiseCopy((resolve, reject) => {dfd.resolve = resolve;dfd.reject = reject;});return dfd;
};

源码

promise 源码已经上传到个人 github ( https://github.com/yizhengfeng-jj/promise 欢迎 star)

参考文章

解读Promise内部实现原理

Promise 源码分析

小邵教你玩转promise源码

❤️爱心三连击1.看到这里了就点个在看支持下吧,你的「点赞,在看」是我创作的动力。
2.关注公众号程序员成长指北,回复「1」加入Node进阶交流群!「在这里有好多 Node 开发者,会讨论 Node 知识,互相学习」!
3.也可添加微信【ikoala520】,一起成长。“在看转发”是最大的支持

这次彻底搞懂 Promise(手写源码多注释篇)相关推荐

  1. 面试前端岗,你被要求手写源码了吗?

    如今前端开发的任职要求越来越高了,不仅要掌握 javascript,熟悉vue.react等各种框架,甚至连后端和 python 都得懂. 前几年只要熟练HTML.CSS.JavaScript ,靠扒 ...

  2. 前端为什么有的接口明明是成功回调却执行了.catch失败回调_前端进阶高薪必看-手写源码篇(高频技术点)...

    前言 此系列作为笔者之前发过的前端高频面试整理的补充 会比较偏向中高前端面试问题 当然大家都是从新手一路走过来的 感兴趣的朋友们都可以看哈 初衷 我相信不少同学面试的时候最怕的一个环节就是手写代码 大 ...

  3. all方法 手写promise_前端进阶高薪必看手写源码篇

    前言 此系列作为笔者之前发过的前端高频面试整理的补充 会比较偏向中高前端面试问题 当然大家都是从新手一路走过来的 感兴趣的朋友们都可以看哈 初衷 我相信不少同学面试的时候最怕的一个环节就是手写代码 大 ...

  4. fortran中call的用法_手写源码系列(一)——call、apply、bind

    什么是手写源码 平时面试时经常会遇到让手写一个已有方法的实现,其实面试官是想考察你对于JS底层逻辑是否熟悉,经常面试会出的会在下面: call.apply.bind promise requireJS ...

  5. TransmittableThreadLocal的简单使用 + 手写源码

    文章目录 使用方法 原理 手写源码 之前有一篇文章谈到链路追踪场景下,需要在异步线程之间,实现跨线程的ThreadLocal传递, 简单场景可以用InheritableThreadLocal,但ITL ...

  6. 【设计模式-手写源码-附1】-简单工厂模式-基于魔兽争霸冰封王座

    1:主题拆解 ①依赖倒置原则-SimpleFactory ②简单工厂+ 配置文件=可配置 ③简单工厂+ 配置文件+反射=可配置可扩展 ④简单工厂升级IOC控制反转 2:基本介绍 ①学习设计模式的套路: ...

  7. 【手写源码-设计模式6】-适配器模式-基于IPhoneX手机充电场景

    1:主题拆解 ①基本介绍 ②IPhone手机充电场景模拟 ③类适配器与对象适配器的区别 ④适配器模式的优缺点 ⑤适用场景 ⑥C#.NET中的使用场景 2:基本介绍 Adapter适配器模式,将一个类的 ...

  8. 【手写源码-设计模式15】-责任链模式-基于人事请假单工作流场景

    1:主题拆解 ①基本介绍 ②人事请假单工作流模拟 ③责任链模式的优缺点 ④适用场景 ⑤应用实例 ⑥ASP.NET 管道模型 2:基本介绍 责任链模式很像异常的捕获和处理,当一个问题发生的时候,当前对象 ...

  9. 读PythonRobotics StateLatticePlanner源码-代码注释篇

    文章目录 2.注释 2.1motion_model.py 2.2model_predictive_trajectory_generator.py 2.3 lookuptable_generator.p ...

最新文章

  1. 2020-12-08 Halcon初学者知识:【3-1】Halcon的语法
  2. WPF使用Animation仿WeChat(微信)播放语音消息
  3. Python Dataframe转List
  4. EM(期望极大化)算法及其推广
  5. 【pytorch】torch.cat()函数
  6. 新的“钉子户”来了!一加9RT将于10月13日正式亮相
  7. 指南-AT应用指南-AT指令指南-音频播放和TTS
  8. netty权威指南(第二版)对应的源码
  9. vue+阿里的G2图表-antv+折线图
  10. greenplum segment down 实例
  11. the win16 subsystem was unable to enter protected mode,DOSX.EXE must be in your AUTOEXEC.NT and pres
  12. docker 中安装 MySQL 以及使用
  13. jwt生成token与解析token
  14. XStream的使用方法
  15. 机器学习第11章 特征选择与稀疏学习
  16. c++ map的遍历
  17. 关于欧拉角和Gimbal lock
  18. comsol学习---声学与振动建模-----2
  19. ACL的概述以及命令应用
  20. Qt creator中调试 变更库一些小讨巧

热门文章

  1. A Survey of Learning on Small Data(二)
  2. day04爬取豌豆荚
  3. Java精进-手写持久层框架
  4. thinkpad x230评测_蘑菇爱搞机 篇五十九:手持联想Thinkpad X230的我,为什么没有升级笔记本——小米Air全金属超轻薄笔记本 体验评测...
  5. CPU卡加密系统与M1加密系统比较!
  6. ITK 数据表达(图像)
  7. Python openjudge 练习025:正常血压
  8. usb接口驱动_TCP/IP Over USB 用USB传输以太网数据,给你的MCU加个网卡
  9. 计算机在F1的应用,电脑入门,F1到F12有什么用
  10. bootstrap checks failed [1]: the default discovery settings are unsuitable for production use; at l