上节课我们介绍了Promise中的一些关键问题,也说了这些问题是作为后续学习Promise封装的基础支撑的,那么接下来我们就开始学习一下关于Promise的自定义封装。所谓自定义封装其实就是手写Promise。用纯原生js代码来实现Promise

初始结构搭建

我们想要手写一个Promise底层实现可想而知是一件比较困难的事情,所以我们把整个Promise来拆分开逐一介绍,首先第一步我们来从搭建初始结构开始。

let p = new Promise(
  (resolve, reject) => {
    resolve("OK");
  }
);
p.then(
  value => console.log(value),
  reason => console.log(reason)
);

我们来看一下上面这段代码是正常情况下我们来创建的一个Promise对象,这一段代码在控制台中执行肯定是输出OK,我们来看一下:

image-20220315141248978

但是现在我们要自定义的话那肯定要把内置的Promise覆盖掉,那么我们来自定义一个js,并在我们的html文件中引入

// promise.js
function Promise() {

}

首先,Promise是什么?我们在之前的内容中介绍过,Promise是一个构造函数,我们可以通过这个构造函数来实例化一个对象。那么我们现在就定义一个函数。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Promise Encapsulation</title>
  <script src="./js/promise.js"></script>
</head>

<body>
  <script>
    let p = new Promise(
      (resolve, reject) => {
        resolve("OK");
      }
    );
    p.then(
      value => console.log(value),
      reason => console.log(reason)
    );
  </script>
</body>

</html>

然后我们在html文件中引入这个js文件,我们来看一下能不能成功覆盖内置的Promise构造函数:

image-20220315141902394

控制台里面报错了,说p.then不是一个函数,这是为什么呢?这说明我们已经成功用我们自己定义的Promise函数覆盖了内置的Promise函数了,而我们自己定义的Promise函数就只是一个空的函数,也没有为我们自己定义的Promise函数添加then方法。所以说这里报错了,那么我们就给Promise函数添加一个then方法:

function Promise(excutor) {

}

Promise.prototype.then = (onResolved, onRejected) => {

};

我们现在再来看代码,首先我们给Promise函数添加了一个形参,为什么要添加这个形参?因为我们在实例化Promise对象的时候要传入一个执行器函数来执行异步任务或者同步任务。其次我们给Promise的原型对象上添加了一个then方法,then方法是用来指定回调的,要接收两个函数类型的参数,所以我们也加了两个形参,现在我们再来看一下效果:

image-20220315142735791

这次没有报错,但是控制台中也没有输出任何东西,因为我们虽然定义了then方法,但是我们并没有做任何操作,所以只是使语法上完成了初始结构的搭建。

以上便是一个Promise的最初始的一个基本结构。

resolvereject函数初始结构搭建

我们现在已经有了一个Promise的基本解构了,但是感觉好像还是还差点意思,我们在实例化Promise对象的时候是不是要传进去一个执行器函数(excutor)啊?而且我们之前在介绍的时候也说过我们的执行器函数在实例化Promise对象的时候会立即被同步调用,那么就简单了:

function Promise(excutor) {
  excutor();
}

我们直接这样同步调用一下不就行了嘛,当然这样的思路是对的,但是大家还记不记得我们传入的执行器函数都接收了两个形参?而且这两个形参都是函数,一个是resolve函数,另一个是reject函数。那么好,我们这样改一下代码:

function Promise(excutor) {
  excutor(resolve, reject);
}

但是这样能行吗?

image-20220315143805460

直接报错了,说resolve没有被定义,那么我们是不是就要来声明一下这两个函数呢?我们来修改一下代码:

function Promise(excutor) {
  const resolve = data => { };
  const reject = data => { };
  excutor(resolve, reject);
}

大家来看这段代码,我们声明了两个箭头函数,分别是resolvereject,而且这两个函数在声明的时候都分别接收了一个形参,因为我们在执行器函数中开启同步任务或者异步任务的时候都是要传入我们异步任务成功或者失败时候的值的,所以需要预留一个形参的位置。那么我们现在再看一下:

image-20220315144258057

现在我们再看控制台里不再报错了,这样的话我们就完成了resolve函数和reject函数的结构搭建

resolve函数与reject函数的实现

我们现在已经实现了resolvereject的基本机构了,但是我们这两个函数存在的目的是什么呢?当然是用来修改Promise对象的状态和Promise对象的结果值,但是我们现在就一个空函数那么肯定是没有办法修改Promise对象的状态和结果值的。

那么接下来我们就来实现这两个函数:

function Promise(excutor) {
  this.PromiseState = "pending";
  this.PromiseResult = null;

  const resolve = data => {
    this.PromiseState = "fulfilled";
    this.PromiseResult = data;
  };

  const reject = data => {
    this.PromiseState = "rejected";
    this.PromiseResult = data;
  };

  excutor(resolve, reject);
}

我们来看这段代码,第一步我们先用this关键字给Promise添加了两个属性,分别就是Promise的状态和结果,为什么要添加这一步呢?因为我们Promise对象在第一步实例化的时候是不是有默认状态和结果的?默认是pending状态,结果是null,所以说我们要在第一步来添加默认状态和结果。

然后再来看resolve函数和reject函数,这两个函数接收一个形参,根据我们之前的学习都知道,这个形参就是Promise对象的结果值,而这两个函数对状态的修改是固定的,不需要参数传入,那么我们直接再通过this关键字将对应的属性做一下修改就可以完成了。

let p1 = new Promise(
  (resolve, reject) => {
    resolve("OK");
  }
);
let p2 = new Promise(
  (resolve, reject) => {
    reject("error");
  }
);
console.log("p1: ", p1);
console.log("p2: ", p2);

但是我们现在还没有去实现then方法的内部代码,那么现在我们来打印一下看看我们的resolve函数和reject函数能不能正常工作:

image-20220315151051520

我们可以看到控制台中正常输出了我们实例化的Promise对象。那么这样我们便成功实现了resolve函数和reject函数的内部代码逻辑。

throw异常处理

我们在之前的内容中说过,修改Promise对象的状态以及结果的方法有三种,分别是resolve函数,reject函数,以及throw关键字,现在我们已经实现了前面两个方法,那么接下来我们来实现第三个throw异常的方式来修改Promise对象的状态和结果。

let p = new Promise(
  (resolve, reject) => {
    throw "error";
  }
);
console.log(p);

我先来看一下当前状态下如果我们throw一个异常出来会是什么情况:

image-20220315151840682

现在是直接报错的,连后面的console.log都没有执行,首先这肯定是不对的,我们不能让throw在这里中断程序,那么如何才能让一段程序执行过程中报了错但是不会中止程序的运行呢?那么我们下意识是不是就会想到try/catch?那么这个try/catch应该写在哪呢?

我们来思考一个问题,这个异常是在哪里抛出的?是不是在执行器函数里面?那么执行器函数是在哪里被执行的呢?是不是在Promise构造函数中?那么我们这么改一下代码:

function Promise(excutor) {
  this.PromiseState = "pending";
  this.PromiseResult = null;
  const resolve = data => {...};
  const reject = data => {...};
  try {
    excutor(resolve, reject);
  } catch (e) {
        reject(e);
  }
}

我们拉看一下这段代码,我们在try部分来执行excutor函数,当excutor函数抛出异常之后,就会被catch捕获,而我们throw出来的异常字符串就会直接被catch的形参e接收到,那么我们怎么样来修改Promise对象的状态和结果呢?是不是直接调用一下reject函数就可以实现了?我们来看一下:

image-20220315152706505

控制台输出的正是一个失败的Promise对象,而且结果值就是我们抛出的异常字符串。

以上我们便实现了所有修改Promise对象的状态和结果的三种方法。

实现Promise状态只能修改一次

我们在之前的学习中提到过,任何一个Promise对象的状态只能修改一次,只能单方向地从pending变为resolved或者rejected,而且不允许resolvedreject之间相互转换,但是我们来看一下我们现在的情况下是什么情况:

let p = new Promise(
  (resolve, reject) => {
    resolve("OK");
    reject("error");
  }
);
console.log(p);

我们在这个执行器函数中既调用了resolve函数又调用了reject函数,按照Promise的设计哲学,我们这里输出的应该是一个成功的Promise对象才对,那么我们来看一下结果是什么样子:

image-20220315153834338

我们发现控制台中输出了一个失败的Promise对象,这是不符合我们的预期的,我们来回想一下Promise的设计哲学,只允许修改pending状态的Promise对象的状态,且不可逆,不可逆我们已经做到了,我们没有实现将其他状态改为pending状态的方法。那么我们就要做一件事,禁止修改状态不是pendingPromise对象的状态,那么简单,只要加一个判断就行了:

function Promise(excutor) {
  this.PromiseState = "pending";
  this.PromiseResult = null;
  const resolve = data => {
    if (this.PromiseState !== "pending") return;
    this.PromiseState = "fulfilled";
    this.PromiseResult = data;
  };
  const reject = data => {
    if (this.PromiseState !== "pending") return;
    this.PromiseState = "rejected";
    this.PromiseResult = data;
  };
  try {
    excutor(resolve, reject);
  } catch (e) {
    reject(e);
  }
}

我们来看一下代码,现在我们在resolve函数和reject函数中都加了一行判断,在修改状态之前做一下判断,只要状态不是pending那么我们直接中止函数执行。那么我们现在再来看一下刚才代码的结果:

image-20220315154509670

现在正如我们预期一样输出的是一个成功的Promise对象。到这一步我们便实现了Promise的状态只能修改一次的规则。

then方法调用回调函数

我们目前已经实现了很多属性和规则了,但是现在我们还有一个核心的功能并没有实现,就是then方法,我们虽然实现了then方法的基本结构,已经可以指定回调了,但是我们的回调还并没有被执行。那么接下来我们就来实现以下。

首先我们要弄清楚我们的回调是在哪里被执行的,首先如果Promise执行器函数中如果执行的是同步任务,那么是先修改状态,然后再指定回调,紧接着马上执行回调,那么我们先来实现同步任务的回调执行,这个时候回调是在哪里执行的呢?是不是在then方法中执行的?那么我们来修改一下代码:

Promise.prototype.then = (onResolved, onRejected) => {
  if (this.PromiseState === "fulfilled") {
    onResolved(this.PromiseResult);
  }
  if (this.PromiseState === "rejected") {
    onRejected(this.PromiseResult);
  }
};

我们来看一下这段代码,我们用两个形参来接收回调函数,分别是成功时的回调和失败时的回调,但是我们能直接就在函数体中调用这两个函数吗?是不是不能?因为我们要根据状态来调用对应的回调。所以说我们根据状态来来做一个判断。那我们来看一下效果:

image-20220315160839447

控制台中好像还是什么都没有输出,这是为什么呢?我们这里then方法是不是一个箭头函数?箭头函数又自己的this吗?是不是没有?如果箭头函数中要用this关键字,会去箭头函数的上一层作用于来找this,这个箭头函数的外层中的this是什么呢?这里的this其实是window,所以说我们这里根本没有取到this.PromiseStatethis.PromiseResult,那么我们修改一下代码:

Promise.prototype.then = function (onResolved, onRejected) {
  if (this.PromiseState === "fulfilled") {
    onResolved(this.PromiseResult);
  }
  if (this.PromiseState === "rejected") {
    onRejected(this.PromiseResult);
  }
};

这次我们不用箭头函数,而是直接用匿名函数再来看一下结果:

image-20220315161252743

这一次我们成功输出了我们的回调执行结果,那么我们让我们的Promise对象是一个失败的Promise再看一下结果:

image-20220315161512960

也成功调用了我们指定的回调,那么这次我们再来抛出异常试一下:

image-20220315161633219

这次我们也可以成功输出我们的回调执行结果。以上便是同步任务时then方法执行回调的实现。

但是有一个问题,为什么我们用匿名函数可以呢?因为匿名函数是有自己的this的,而我们then方法是不是通过实例对象来调用的?所以说用匿名函数的this就是实例对象自身,而PromiseStatePromiseResult属性就在实例对象上,那么就可以通过this关键字取到。

异步任务回调执行

既然讲完了同步任务的回调执行,那么接下来肯定就是大家都好奇的异步任务回调的执行了,因为Promise是解决异步任务的一个新方案,那么肯定用Promise封装异步任务的使用场景更多。我们先来看当前状态:

let p = new Promise(
  (resolve, reject) => {
    setTimeout(
      () => resolve("OK"),
      1000
    )
  }
);
p.then(
  value => console.log(value),
  reason => console.warn(reason)
);

我们还是用一个定时器来模拟一个异步任务,让Promise对象的状态延迟 1 秒再改变,那么我们来看一下结果:

image-20220315162645304

控制台什么都没有输出,这是为什么呢?按照同步任务的情况来说,应该会输出OK才对啊。我们来分析一下,当我们实例化完Promise对象的时候,状态改变了没有?是不是还没有,因为这是一个异步任务,要等 1 秒钟才会修改状态,但是p.then方法会等异步任务执行完再调用吗?肯定不会的啊,那么这个时候就会直接指定回调函数,并且调用,可是这个时候Promise的状态还没有发生改变,依然还是pending,而我们在then方法中只对fulfilledrejected做了判断,所以导致then方法并没有执行任何一个回调。

那么我们来思考一个问题,从Promise的设计哲学来说,我们不论Promise中开的是同步任务还是异步任务,我们执行回调的前提条件是不是Promise的状态已经发生改变了?那么异步任务会在什么时候修改状态呢?是不是当异步任务执行完成并执行resolve或者reject函数的时候才会修改状态?

我们在讲同步任务的时候提到过,首先我们要弄清楚我们的回调是在哪里被执行的,异步任务完成后调用resolve或者reject函数修改状态然后调用回调,但是这个时候then方法已经执行完了,所以说有没有一种可能异步任务的回调是在resolve或者reject函数中被执行的呢?比如这样:

function Promise(excutor) {
  this.PromiseState = "pending";
  this.PromiseResult = null;
  const resolve = data => {
    if (this.PromiseState !== "pending") return;
    this.PromiseState = "fulfilled";
    this.PromiseResult = data;
    onResolved(this.PromiseResult);
  };
    ...
}

但是,有一个问题,这个onResolvedonRejected都是传给then方法的,我们的这个Promise函数中的resolvereject函数是不是根本就拿不到啊?那么我们是不是应该在then方法上做一个手脚,当我们异步任务还没有完成,Promise状态还没有改变的时候,then方法已经被调用了,但是then方法中并没有做对pending状态的判断,那么我们就把这个判断加上,但是这个判断里面执行什么呢?我们先来看代码看大家能不能明白:

function Promise(excutor) {
  ...
  this.callback = {};
  const resolve = data => {
    ...
    if (this.callback.onResolved) {
      this.callback.onResolved(this.PromiseResult);
    }
  };
  const reject = data => {
    ...
    if (this.callback.onRejected) {
      this.callback.onRejected(this.PromiseResult);
    }
  };
    ...
}

Promise.prototype.then = function (onResolved, onRejected) {
  ...
  if (this.PromiseState === "pending") {
    this.callback = { onResolved, onRejected };
  }
};

我们看见我们在Promise函数中添加了一个属性叫callback,这个属性是一个对象,然后我们在then方法中判断如果是pending状态的话就把我们传给then方法的两个回调函数都保存到Promise对象的callback属性里,这样的我们在resolve以及reject函数中来判断callback属性中的onResolvedonRejected是否为空,如果不为空就调用对应的回调函数,我们现在来看一下结果:

image-20220315170019917

image-20220315170047517

现在我们异步任务的then以及回调函数的执行也完成了。

指定多个回调的实现

我们现在实现了同步任务和异步任务回调的执行,那么还有一个问题,我们如果要指定多个回调函数的话该怎么实现呢?首先我们现在肯定是不行的我们来看一下:

let p = new Promise(
  (resolve, reject) => {
    setTimeout(
      () => resolve("OK"),
      1000
    )
  }
);
p.then(
  value => console.log(value),
  reason => console.warn(reason)
);
p.then(
  value => console.log(value, value),
  reason => console.warn(reason, reason)
);

我们现在是这个异步任务,然后我们每个状态都指定了两个回调,那么我们看一下结果是什么:

image-20220315171411336

首先回调执行了,但是好像只执行了第二次指定的回调,这是为什么呢?因为我们每次调用then方法,如果状态为pending,就都把回调都放在一个对象中并且存储到Promise对象的callback属性中,当我们第二个回调指定的时候,Promise的状态还没有改变,所以第二次存储的时候覆盖了第一次存储的属性。那么这么看来我们then方法方法就不太合理了。那么我们这么修改一下:

function Promise(excutor) {
  ...
  this.callbacks = [];
  const resolve = data => {
    ...
    this.callbacks.forEach(item => item.onResolved(this.PromiseResult));
  };
  const reject = data => {
    ...
    this.callbacks.forEach(item => item.onRejected(this.PromiseResult));
  };
  ...
}

Promise.prototype.then = function (onResolved, onRejected) {
  ...
  if (this.PromiseState === "pending") {
    this.callbacks.push({ onResolved, onRejected });
  }
};

我们来看一下代码我们把callback属性改成了callbacks,并且改成了数组,那么我们在then方法中就不用赋值了,我们直接给push到数组中就好了,那么我们在resolvereject函数中就不能像原来那样调用回调了,我们要遍历数组来执行每一个回调。那么我们来看一下结果:

image-20220315172330611

image-20220315172402158

以上便实现了给异步任务指定多个回调。

可能有人要问了,那么同步任务不用再处理then方法吗?我们来分析一下,同步任务是不是先改变状态然后再指定回调啊?那么根本不需要把回调存到对象属性中了啊。指定完直接就执行掉了。

本节课的内容暂时就先到这,其他的封装方面的内容我们在下节课再继续介绍。

Copyright statement:The articles of this site are all original if there is no special explanation, indicate the source please when you reprint.

Link of this article:https://work.lynchow.com/article/encapsulation_of_promise/