如何从无到有实现Promise(上)

前言

最近整理了两篇关于js异步的笔记,谈到异步就不得不说说 PromisePromise 取代传统回调方式实现异步,也是理解 generatorasync/await 的前提。
基本用法不再阐述,这里只谈谈如何从无到有的实现 Promise

一、从构造函数开始

我们在使用 Promise 时都要使用 new 关键字,由此可知 Promise 其实就是一个构造函数,我们使用这个构造函数创建一个 Promise 实例。
该构造函数很简单,它只有一个参数,按照 Promise/A+ 规范的命名,把 Promise 构造函数的参数叫做 executorexecutor 类型为函数。这个函数又天生的具有 resolve、reject 两个方法作为参数。
构造函数的定义如下:

 function Promise(executor) {

 }

二、初见本质

通过一个简单的例子复习下基本的用法

      var getPic = url =>
        new Promise((resolve, reject) => {
          // ... some code
          if (/* 异步操作成功 */) {
            resolve("success value");
          } else {
            /* 异步操作失败 */
            reject("sth wrong");
          }
        });

      getPic("1.png").then(
        data => {
          console.log(`加载完成${data}`);
        },
        error => {
          console.log(error);
        }
      );

观察例子,剖析本质

  • Promise 构造函数返回一个 promise 对象实例,这个返回的 promise 对象具有一个 then 方法。
  • Promise/A+ 规范可知,then 方法中有两个参数,都是函数类型,分别是 onfulfilledonrejected。**
  • 函数 onfulfilled 可以通过参数获取到 promise 对象 resolved 的值,onrejected 函数可以通过参数获取到 promise 对象的 rejected 的值。**

在此基础上继续完善我们的 Promise,添加原型方法 then

      function Promise(executor) {

      }

      Promise.prototype.then = function(onfulfilled, onrejected) {

      };

这里可能有人会有疑问,then 方法为何要添加在 Promise 构造函数的原型上,而不是作为实例方法?

简单来说就是每个 promise 实例的 then 方法逻辑是完全一致的,在实例调用该方法时,可以通过原型(Promise.prototype)找到,如果作为实例方法,那么每次实例化都会为每个实例新创建一个 then 方法,而这些 then 方法又是一样的,这样显示不合理,并且会浪费内存。

总结:
通过 new 关键字调用 Promise 构造函数时,在执行完逻辑后,调用上文所说的构造函数参数 executor 中的 resolve 方法,并将需要返回的值作为 resolve 函数参数执行。并且这个返回的值,可以在 then 方法的第一个参数(onfulfilled函数)中拿到。
同理当出现错误时调用 executor 中的 reject 方法,也可将需要返回的错误信息作为 reject 函数参数执行。这个错误的信息就可以在 then 方法的第二个参数(onrejected函数)中拿到。

那么我们就需要俩个变量,分别存储 resolve 的值和 reject 的值。与此同时,还需要一个变量来存储 promise 的状态(pending、fulfilled、rejected)。最后构造函数中还要有 resolve 以及 reject 方法,并且要作为构造函数参数(executor)的参数提供给调用者使用。

根据我们总结的点继续完善 Promise

      function Promise(excutor) {
        this.status = "pending";
        this.resolveVal = null;
        this.rejectVal = null;

        const resolve = value => {
          this.resolveVal = value;
        };

        const reject = error => {
          this.rejectVal = error;
        };

        excutor(resolve, reject);
      }

      Promise.prototype.then = function(
        onfulfilled = Function.prototype,
        onrejected = Function.prototype
      ) {
        onfulfilled(this.resolveVal);

        onrejected(this.rejectVal);
      };

三、状态管理不可少

promise 实例的状态具有单一,不可逆的特征,就是说 promise 实例的状态只能从 pending 改变为 fulfilled,或者从 pending 改变为 rejected,并且一旦改变,不能再次更改。不过我们目前的实现无法满足这一需求,如果我们先后调用 resolve('value'); reject('error');status 的状态会先更改为 fulfilled 之后再变更为 rejectedthen 方法中的两个函数参数都会执行。
要在代码中加入状态的变更以及判断:

      function Promise(excutor) {
        this.status = "pending";
        this.resolveVal = null;
        this.rejectVal = null;

        const resolve = value => {
          if (this.status === "pending") {
            this.resolveVal = value;
            this.status = "fulfilled";
          }
        };

        const reject = error => {
          if (this.status === "pending") {
            this.rejectVal = error;
            this.status = "rejected";
          }
        };

        excutor(resolve, reject);
      }

      Promise.prototype.then = function(
        onfulfilled = Function.prototype,
        onrejected = Function.prototype
      ) {
        if (this.status === "fulfilled") {
          onfulfilled(this.resolveVal);
        }

        if (this.status === "rejected") {
          onrejected(this.rejectVal);
        }
      };

这样一来,promise 实例的状态只会变更一次,满足我们的需求了。

四、我可是解决异步问题的

目前为止,我们的实现有一个最大的问题,promise 是用来解决异步问题的,但是目前的代码都是同步执行的,貌似缺少了最关键的逻辑。
如果尝试使用我们的 Promise 做点什么就会发现问题:

      let promise = new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("value");
        }, 3000);
      });

      promise.then(data => {
        console.log(data);
      });

正常来讲,代码的执行结果应该为,3秒后打印出“value”,但是结果是并未输出任何信息。

如果你读过我之前的异步笔记,就会很容易的发现问题,通过 new 去调用 Promise 构造函数的时候,在 setTimeout 中调用了 resolve 方法。也就是说3秒后才去调用的 resolve 方法,并且在此时才去更改的 status 的状态。并且我们所实现的 then 方法中的 onfulfilledonrejected 也完全是同步的,相当于 onfulfilled 在执行的时候,this.status 的值依然是 pending ,等到3秒后状态更新了,但是 onfulfilled 也早已执行完毕了。

解决这个问题的关键在于,我们要在合适的时间再去调用 onfulfilled 方法,这就需要在 then 方法中做一些修改,在执行到 then 的时候,如果 statuspending,那么就将用户传进来的 onfulfilledonrejected 保存起来,等到 resolve 或者 reject的时候在拿出来执行。

修改代码如下:

      function Promise(excutor) {
        this.status = "pending";
        this.resolveVal = null;
        this.rejectVal = null;

        // 用于保存 onfulfilled 、onrejected 方法
        this.onFulfilledFunc = Function.prototype;
        this.onRejectedFunc = Function.prototype;

        const resolve = value => {
          if (this.status === "pending") {
            this.resolveVal = value;
            this.onFulfilledFunc(this.resolveVal);
            this.status = "fulfilled";
          }
        };

        const reject = error => {
          if (this.status === "pending") {
            this.rejectVal = error;
            this.onRejectedFunc(this.rejectVal);
            this.status = "rejected";
          }
        };

        excutor(resolve, reject);
      }


      Promise.prototype.then = function(
        onfulfilled = Function.prototype,
        onrejected = Function.prototype
      ) {
        if (this.status === "fulfilled") {
          onfulfilled(this.resolveVal);
        }

        if (this.status === "rejected") {
          onrejected(this.rejectVal);
        }

        // 保存 onfulfilled 、onrejected 方法
        if (this.status === "pending") {
          this.onFulfilledFunc = onfulfilled;
          this.onRejectedFunc = onrejected;
        }
      };

再执行上面的测试代码,可以看到3秒后打印出 “value”,测试通过,我们实现的 Promise 可以支持异步了。

五、我不仅支持异步,我自身也是异步的

Promise 不仅支持异步,并且它的成功或失败的回调也要异步执行,用一段代码测试下我们实现的 Promise

      let promise = new Promise((resolve, reject) => {
        resolve("value");
      });

      promise.then(data => {
        console.log(data);
      });

      console.log("1");

根据我们所知的情况,应该先输出 1 ,然后再输出 value。但执行后发现是先输出 value,然后再输出 1。

这样一来,promise 的回调就是同步执行的了,我们需要做的是将 resolvereject 放到任务队列中执行。
这里最严谨的做法是,为了保证 Promise 属于 microtasks,很多 Promise 的实现库用了 MutationObserver 来模仿 nextTick
我们偷个懒,先使用不那么严谨的 setTimeout 实现这个效果。将 resolvereject 方法里的内容用 setTimeout 包裹起来即可。

六、细节完善

又经过我的一番测试,发现了几个小问题,逐一修复。

  1. 比如在 promise 实例状态变更之前,添加了多个 then 方法,那么第二个 then 中的 onFulfilledFunc 会覆盖第一个 then 中的 onFulfilledFunc

解决办法:将所有 then 方法中的 onFulfilledFunc 储存为一个数组 onFulfilledArray,在 resolve 时,依次执行即可。

  1. 如果在构造函数中出错,promise 实例应该将状态更改为 rejected

解决办法:try…catch 块对 executor 进行包裹。

到此为止,简易版的 Promise 就实现了,附上完整代码:

      function Promise(excutor) {
        this.status = "pending";
        this.resolveVal = null;
        this.rejectVal = null;
        this.onFulfilledFuncArray = [];
        this.onRejectedFuncArray = [];

        const resolve = value => {
          setTimeout(() => {
            if (this.status === "pending") {
              this.resolveVal = value;
              this.status = "fulfilled";
              this.onFulfilledFuncArray.forEach(fn => {
                fn(this.resolveVal);
              });
            }
          }, 0);
        };

        const reject = error => {
          setTimeout(() => {
            if (this.status === "pending") {
              this.rejectVal = error;
              this.status = "rejected";
              this.onRejectedFuncArray.forEach(fn => {
                fn(this.rejectVal);
              });
            }
          }, 0);
        };

        try {
          excutor(resolve, reject);
        } catch (error) {
          reject(error);
        }
      }


      Promise.prototype.then = function(
        onfulfilled = Function.prototype,
        onrejected = Function.prototype
      ) {
        if (this.status === "fulfilled") {
          onfulfilled(this.resolveVal);
        }

        if (this.status === "rejected") {
          onrejected(this.rejectVal);
        }

        if (this.status === "pending") {
          this.onFulfilledFuncArray.push(onfulfilled);
          this.onRejectedFuncArray.push(onrejected);
        }
      };

下一篇《如何从无到有实现Promise(下)》中将会继续实现 Promise ,完善我们的 Promise 并为它添加静态方法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容