从0到1实现A+规范Promise(上篇)

吐槽君 分类:javascript

    Promise在日常开发中使用非常广泛,得益于其灵活的异步操作处理机制,我们对异步操作(尤其是具有依赖关系的异步操作)的处理大为简化。而了解其底层运行机制将有助于我们更灵活的使用Promise。本文旨在记录/总结我实现Promise的过程并分享思路。其中上篇介绍Promise基本功能与then方法的实现,下篇介绍其他实例方法与静态方法的实现。

在开始之前首先要说明几点

1. 本文适合对Promise有一定了解且有使用经验的小伙伴食用。关于Promise的基本使用我在前面有过介绍。
2. 我们本次实现的Promise是完全按照PromiseA+规范来实现的。不了解PromiseA+规范的小伙伴可以先参考一下。ES6中的Promise就采用了该规范。
3. 我们的总体思路是,首先回顾在日常开发中,Promise的某个功能点是如何使用的。进一步思考如何实现,做到有的放矢。

源码地址,欢迎star?

一. 搭建初始结构

    我们首先来搭建初始结构,在使用promise时,首先要在其构造函数传入executor函数,我们称之为执行器函数。执行器函数接收两个参数resolve,reject,这同样是两个函数,我们用它们来改变Promise的状态和结果。执行器函数同步执行,若执行过程中抛出错误,则promise立即变为失败状态。
而promise的状态有三种,分别是pending(等待),fulfilled(成功),rejected(失败)。状态只能从等待转变为成功/失败,且只能改变一次。

因此实现思路也就有了

  • Promise构造函数中需要传入一个executor函数,默认立即同步执行,若执行中抛出错误,立即执行reject()。
  • Promise内部提供两个方法 resolve(成功)、reject(失败) ,可以更改Promise的状态和结果。
  • Promise有3个状态: (等待pending、成功fulfilled、失败rejected)。

我们按照上述搭建一下初始结构

// 声明Promise的三种状态
const Pending = "pending"; // 等待
const Fulfilled = "fulfilled"; // 成功
const Reject = "rejected"; // 失败

function Promise(executor) {
  // 初始时为等待状态
  this.PromiseState = Pending;
  // 存储promise的结果
  this.PromiseResult = null;
  // 存储成功/失败的回调函数 后面会介绍如何使用
  this.callbacks = [];
  const resolve = () => {};
  const reject = () => {};
  // 同步执行执行器函数 若抛出错误 则执行reject()
  try {
    executor(resolve, reject);
  } catch (e) {
    reject(e);
  }
}
 

二.实现resolve/reject

我们知道resolve/reject的职责有两点

  • 改变Promise的状态
  • 将传入方法的值设置为Promise的结果。

我们据此来实现

 const resolve = (data) => {
 // 要注意 Promise的状态只能修改一次 因此一旦发现Promise的状态已经改变 就不再继续向下执行
    if (this.PromiseState !== Pending) return;
    // 修改状态
    this.PromiseState = Fulfilled;
    // 设置结果值
    this.PromiseResult = data;
   
  };
  const reject = (data) => {
    if (this.PromiseState !== Pending) return;
    this.PromiseState = Reject;
    this.PromiseResult = data;
  };
 

三.添加实例方法 then

then方法的功能以及实现比较复杂,我们分几个步骤进行。

1.添加对回调函数的处理

    首先,我们使用then方法时,通常会传入两个回调函数(当然也可以只指定其中一个或都不指定),分别是Promise成功/失败后的回调。
而then的职责就是根据Promise的状态执行对应的回调函数(这样说其实是不准确的,后面会解释)。

Promise.prototype.then = function(onResolved, onRejected){
    //根据promise的状态 执行相应的回调函数
    if(this.PromiseState === Fulfilled){
        // 调用回调函数时,传入promise的结果,也即调用resolve/reject时传入的参数
        onResolved(this.PromiseResult);
    }
    if(this.PromiseState === Reject){
        onRejected(this.PromiseResult);
    }
}
 
2.完善then方法,添加对异步任务的回调处理。

    目前实现的then方法存在缺陷。它只能解决同步调用resolve/reject的情况。我们来捋一下。

    我们知道resolve/reject方法会改变Promise的状态,因此当resolve/reject同步执行(即执行器函数中进行的是同步操作)时,会导致执行then方法时resolve/reject已经执行完毕,即此时Promise一定已经改变,可以顺利执行then指定的回调。

    但当resolve/rejetc异步调用,换句话说,我们在执行器函数中进行的是异步操作。这会导致resolve/rejetc的调用操作会进入任务队列。因此当执行then方法时,Promise的状态没有改变。仍然是'pending'。而我们知道,成功/失败的回调函数是一定要等到Promise的状态改变后再执行的。怎么办呢?
这时候,我们在Promise构造函数中声明的callbacks数组就排上了用场。我们可以在then方法中判断,当状态为'pending'时,将成功/失败的回调推入该数组中。等将来Promise状态改变时(也就是resolve/reject调用时)再取出来调用。因此我们也要进一步完善resolve/reject方法,使其能够在callbacks中存有回调时,循环调用。
为什么callbacks是个数组呢,这样可以允许我们指定多个then方法。

首先完善then方法

Promise.prototype.then = function(onResolved, onRejected){
    //根据promise的状态 执行相应的回调函数
    if(this.PromiseState === Fulfilled){
        // 调用回调函数时,传入promise的结果,也即调用resolve/reject时传入的参数
        onResolved(this.PromiseResult);
    }
    if(this.PromiseState === Reject){
        onRejected(this.PromiseResult);
    }
    //pending状态时,暂存回调函数
    if(this.PromiseState === Pending){
        this.callbacks.push({
            onResolved,
            onRejected
        })
    }
}
 

完善reject/resolve

const resolve = (data) => {
    if (this.PromiseState !== Pending) return;
    this.PromiseState = Fulfilled;
    this.PromiseResult = data;
    // 判断是否有暂存的回调函数 有则循环调用
    if (this.callbacks.length > 0) {
      this.callbacks.forEach((cb) => cb.onResolved(data));
    }
  };
const reject = (data) => {
    if (this.PromiseState !== Pending) return;
    this.PromiseState = Reject;
    this.PromiseResult = data;
      if (this.callbacks.length) {
        this.callbacks.forEach((cb) => cb.onRejected(data));
      }
  };
 
3.继续完善then。

    在A+规范中约定,Promise的then方法会返回一个Promise,且该Promise的状态由then方法中指定的回调函数的返回结果决定。经过上面的讨论我们知道,then中的回调执行要分同步与异步两种情况。同样,我们这里也分开讨论。

    同步的情况比较简单,由于调用then时,Promise的状态已经改变,因此我们只需调用相应的回调函数并拿到其执行结果。根据结果来决定then方法返回的Promise的状态即可。

Promise.prototype.then = function (onResolved, onRejected) {
  // 创建一个新的Promise 最后返回它
  let promise = new Promise((resolve, reject) => {
    if (this.PromiseState === Fulfilled) {
      try {
        // 拿到回调的执行结果
        let res = onResolved(this.PromiseResult);
        // 若结果是promise 则then的状态和结果由该promise决定
        // 这里的判断条件并不严苛 后面会继续完善
        if (res instanceof Promise) {
          // 若返回结果是Promise 则一定可以执行then方法
          // 我们从then方法中获取回调返回的Promise的状态和结果,将其作为then的状态和结果
          res.then(
            (v) => {
              resolve(v);
            },
            (r) => {
              reject(r);
            }
          );
        } else {
          // 若是普通值则直接返回成功的promise并将该值作为结果值
          resolve(res);
        }
        // 执行过程中抛出错误则直接返回失败的promise
      } catch (e) {
        reject(e);
      }
    } else if (this.PromiseState === Reject) {
      try {
        let res = onRejected(this.PromiseResult);
        if (res instanceof Promise) {
          res.then(
            (v) => {
              resolve(v);
            },
            (r) => {
              reject(r);
            }
          );
        } else {
          resolve(res);
        }
      } catch (e) {
        reject(e);
      }
    }
  });
  // 返回该promise
  return promise;
};
 

    接下来讨论异步修改Promise状态时,then返回的Promise的状态和结果问题。我们知道异步修改状态时,成功/失败的回调函数不是在then方法中直接执行。而是会暂存起来,在resolve/reject中执行。而这两个方法是在Promise构造函数中声明的。我们如何才能在构造函数中改变实例方法then的状态呢?这就需要在then暂存回调函数的操作中为回调函数绑定执行上下文

// 我们把根据回调结果决定then的返回状态的操作先简单封装一下 后面会继续完善
function resolvePromise(result, resolve, reject) {
  try {
    if (result instanceof Promise) {
      result.then(
        (v) => {
          resolve(v);
        },
        (r) => {
          reject(r);
        }
      );
    } else {
      // 若是普通值则直接返回成功的Promise并将该值作为结果值
      resolve(result);
    }
  } catch (e) {
    reject(e);
  }
}
Promise.prototype.then = function (onResolved, onRejected) {
  let promise = new Promise((resolve, reject) => {
    if (this.PromiseState === Fulfilled) {
      try {
        let res = onResolved(this.PromiseResult)
        resolvePromise(res, resolve, reject);
      } catch (e) {
        reject(e);
      }
    } else if (this.PromiseState === Reject) {
      try {
        let res = onRejected(this.PromiseResult);
        resolvePromise(res, resolve, reject);
      } catch (e) {
        reject(e);
      }
    } else {
      this.callbacks.push({
      // 对异步操作的回调处理 其行为与上面的同步操作的回调处理行为一致 只是要绑定上下文 否则将来执行时会丢失this
        onResolved: function () {
          try {
            let res = onResolved(this.PromiseResult);
            resolvePromise(res, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }.bind(this), // 绑定上下文
        onRejected: function () {
          try {
            let res = onRejected(this.PromiseResult);
            resolvePromise(res, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }.bind(this),
      });
    }
  });
  // 返回该promise
  return promise;
};
 

四.添加catch方法。

    catch方法主要用来捕获错误,而该方法的一大特性是能够捕获穿透的异常。也就是能捕获在任一阶段抛出的异常。因此我们要解决两个问题

1.捕获错误并执行错误回调。

    这一点比较好实现,catch方法实质上就是特殊的then方法,我们只需要指定失败的回调函数即可。

2.实现异常穿透。

    我们首先要了解穿透的意义是什么,即当链式调用的某个节点抛出了异常,但没指定相应的失败回调,则该错误信息会一直向下传递,直到被catch方法捕获。
而实现穿透的关键在于,如何实现在没指定回调函数的情况下,将状态和结果向下传递。
因此我们要指定回调的默认行为

    默认行为的职责就是将错误传递下去。因为既然要穿透,说明我们没有为前面的错误指定回调。因此才要将错误向下传递,让后面的错误回调来捕获到该错误。因此默认回调的行为也就是将错误信息传递下去。如何传递呢?试想一下
既然要调用错误的回调,说明上一个Promise对象状态为失败了。因此默认回调就是要让它一直错下去!怎么办? 使用throw抛出错误

    我们知道,在then的链式调用过程中,then返回的Promise的状态和结果是由then的回调的返回结果决定的。因此若在默认的失败回调中抛出了错误,则会立即被trycatch捕获到。因此当前的then的返回结果会立即变为失败的Promise且结果是抛出的错误信息。再进一步,由于当前then的返回了失败的Promise,因此下个then一定会执行其失败回调。若下个then指定了失败回调,则前面的错误就被捕获到了。若仍然没指定失败回调,则又会执行默认的失败回调。由此就达到了异常穿透的效果。假设我们在then的链式调用过程中一直没指定失败回调,则最终抛出的错误就会被catch方法捕获。

    同理,成功的状态和结果也可以传递,也就是我们在then的链式调用过程中,即使没有为中间的某个then指定回调函数也不会中断链式调用。其状态和结果会继续向下传递。

接下来实现catch方法和指定then方法的默认回调行为。

Promise.prototype.catch = function (onRejected) {
  // 直接调用then,不传成功的回调
  return this.then(undefined, onRejected);
};
 
Promise.prototype.then = function (onResolved, onRejected) {
  if (typeof onRejected !== "function") {
    onRejected = (reason) => {
    // 抛出异常这将使得下个then继续执行失败回调
      throw reason;
    };
  }
  if (typeof onResolved !== "function") {
  // 返会成功信息 下个then会执行成功回调
    onResolved = (value) => value;
  }
  ......
};
 

五.异步执行回调

    这里要说明一点,我们一般认为Promise的then方法是异步执行的,而且在日常使用中Promise的then方法的行为似乎也印证了这一点。但实际上真正异步执行的是then方法指定的回调函数。可是then方法的职责不就是根据Promise的状态来执行相应的回调吗?事实上经过前面的then方法的实现我们已经知道,then方法本身是同步执行的,当执行then时,若Promise状态已经改变,则会执行回调。若未改变则会将回调暂存。由此可见回调的执行不一定是在then方法中,因此我们说前面对then方法职责的阐述是有有缺陷的。因此要实现回调的异步执行我们不能从then方法下手,而是应对回调函数本身动手脚。实现异步执行的方式有很多,这里就用定时器实现。

//这里就以执行成功的回调为例,我们只需包一层定时器即可。
......
 if (this.PromiseState === Fulfilled) {
      setTimeout(() => {
        try {
          let x = onResolved(this.PromiseResult);
          resolvePromise(res, resolve, reject);
        } catch (e) {
          reject(e);
        }
      });
    }
 ......
 

六.细节问题

至此Promise的基本功能已经完成,接下来完善几个细节问题

1.then方法中成功/失败的回调的返回值问题。具体如下

    当回调的返回值与当前的then方法的返回值引用了同一个promise对象时,会造成死循环,因此应抛出错误。
接下来就是具体判断返回值是不是Promise。我们之前用的instanceof方法不能最准确的判断。由于该回调函数的返回值直接决定了then的状态和结构,因此我们要严格判断它是不是Promise。按照PromiseA+规范,只有当返回值的类型是对象或函数,存在then属性,且then属性是函数时这样才能保证返回值它是Promise。同时还要保证,若resolve/reject同时被调用或被调用多次,只取第一次,其他调用会被忽略。
当resolve/reject返回的仍然是Promise,则递归解析直到为普通值。这块逻辑具体可以参考A+规范文档中对该部分的阐述。

下面来完善resolvePromise方法

function resolvePromise(promise, res, resolve, reject) {
  // 1.回调的返回值和then的返回值不能引用同一个对象 可能造成死循环
  if (promise === res) {
    return reject(new TypeError("不能引用同一个对象"));
  }
  // 该变量为已经调用回调的标志,避免多次调用。
  let called;
  // 2.res是对象或者函数,说明有可能是promise
  if ((typeof res === "object" && res != null) || typeof res === "function") {
    try {
      let then = res.then; // 获取其then属性
      // 存在then属性,且是函数类型,则可以断定是promise
      if (typeof then === "function") {
      // 调用then并绑定上下文
        then.call(
          res,
          (y) => {
            // 避免多次调用
            if (called) return;
            called = true;
            // 若返回值仍是promise 则递归解析直到为普通值
            resolvePromise(promise, y, resolve, reject);
          },
          (r) => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        resolve(res);
      }
    } catch (e) {
    // 若取then或执行then的过程中出错,直接返回失败。
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    resolve(res);
  }
}
 

由于修改了resolvePromise,因此调用该方法的地方也要做出调整。

......
if (this.PromiseState === Fulfilled) {
      setTimeout(() => {
        try {
          let res = onResolved(this.PromiseResult);
          //将当前then方法即将返回的Promise传入
          resolvePromise(promise, res, resolve, reject);
        } catch (e) {
          reject(e);
        }
      });
    }
......
 
2 resolve/reject中传入的仍是Promise。

    上面分析过程中有类似的情况,解决办法就是递归解析直到为普通值。

......
let resolve = (value) => {
        // 增加判断如果resolve传入的是promise的判断
        // 这里无需进行像上面那样苛刻的判断,我们要的只是他的返回值
        if (value instanceof Promise) { // 递归解析直到为普通值为止
              value.then(resolve, reject)
              return
          }
      }
......
 

七.测试

至此Promise的基本功能已经实现。

我们可以用promises-aplus-tests这款插件来测试我们写的promise符不符合A+规范。
分为三步

  • 1 全局安装 npm i -g promises-aplus-tests

  • 2 在我们写的promise.js文件中配置脚本

......
Promise.defer = Promise.deferred = function () {
      let dfd = {}
      dfd.promise = new Promise((resolve, reject) => {
          dfd.resolve = resolve
          dfd.reject = reject
      })
      return dfd
  }
module.exports = Promise;
 
  • 3 运行文件测试 promises-aplus-tests promise.js

只要通过全部测试,则说明我们写的Promise是符合PromiseA+规范的,如下图所示。

image.png

以上就是符合A+规范的Promise的实现过程,在下篇中将继续完成Promise的其他实例方法和静态方法。

参考:github.com/Tie-Dan/Pro…

回复

我来回复
  • 暂无回复内容