如何实现符合 Promise/A+ 规范的 Promise?

如何实现符合 Promise/A+ 规范的 Promise?

  由于 Promise在异步编程中的重要性不言而喻,因此在很多面试中,现场实现 Promise相关方法的题目经常会出现,比如 allrace或者 any等。因此一步步实现一个符合标准的 Promise,希望在遇到相关题目时能够游刃有余。

在开始前请先思考一下:

  1. Promise/A+ 约定了哪些规范?
  2. 在手动实现 Promise的过程中都遇到过哪些问题?

Promise/A+ 规范

  只有对规范进行解读并且形成明确的认知,才能更好地实现 Promise。官方的地址为:https://promisesaplus.com/。

术语

先来看看 Promise/A+规范的基本术语,如下所示。

“promise” is an object or function with a then method whose behavior conforms to this specification.
“thenable” is an object or function that defines a then method.
“value” is any legal JavaScript value (including undefined, a thenable, or a promise).
“exception” is a value that is thrown using the throw statement.
“reason” is a value that indicates why a promise was rejected.

翻译过来,它所描述的就是以下五点。:

  1. promise”:是一个具有 then方法的对象或者函数,它的行为符合该规范。
  2. thenable”:是一个定义了 then方法的对象或者函数。
  3. value”:可以是任何一个合法的 JavaScript的值(包括 undefinedthenablepromise)。
  4. exception”:是一个异常,是在 Promise里面可以用 throw语句抛出来的值。
  5. reason”:是一个 Promisereject之后返回的拒绝原因。

状态描述

Promise的内部状态的描述,如下所示。

A promise must be in one of three states: pending, fulfilled, or rejected.
When pending, a promise:
may transition to either the fulfilled or rejected state.
When fulfilled, a promise:
must not transition to any other state.
must have a value, which must not change.
When rejected, a promise:
must not transition to any other state.
must have a reason, which must not change.
Here, “must not change” means immutable identity (i.e. ===), but does not imply deep immutability.

将上述描述总结起来,大致有以下几点。

  1. 一个 Promise有三种状态:pendingfulfilledrejected
  2. 当状态为 pending状态时,即可以转换为 fulfilled或者 rejected其中之一。
  3. 当状态为 fulfilled状态时,就不能转换为其他状态了,必须返回一个不能再改变的值。
  4. 当状态为 rejected状态时,同样也不能转换为其他状态,必须有一个原因的值也不能改变。

then 方法

  一个 Promise必须拥有一个 then方法来访问它的值或者拒绝原因。

then 方法有两个参数:

promise.then(onFulfilled, onRejected)

onFulfilledonRejected都是可选参数。

  • 如果 onFulfilled是函数,则当 Promise执行结束之后必须被调用,最终返回值为 value,其调用次数不可超过一次
  • onRejected除了最后返回的是 reason外,其他方面和 onFulfilled在规范上的表述基本一样
多次调用

  then 方法其实可以被一个 Promise调用多次,且必须返回一个 Promise对象。then的写法如下所示,其中 Promise1执行了 then的方法之后,返回的依旧是个 Promise2,然后我们拿着 Promise2又可以执行 then方法,而 Promise2是一个新的 Promise对象,又可以继续进行 then方法调用。

promise2 = promise1.then(onFulfilled, onRejected);

一步步实现 Promise

  按照 Promise/A+的规范,第一步就是构造函数。

构造函数

  这一步的思路是:Promise构造函数接受一个 executor函数,executor函数执行完同步或者异步操作后,调用它的两个参数 resolvereject。请看下面的代码,大致的构造函数框架就是这样的。

function Promsie(executor) {
  this.status = "pending"; // Promise当前的状态
  this.data = undefined; // Promise的值
  this.onResolvedCallBack = []; // Promise resolve时的回调函数集
  this.onRejectedCallBack = []; // Promise reject时的回调函数集
  executor(resolve, reject); // 执行executor并传入相应的参数
}

  从上面的代码中可以看出,我们先定义了一个 Promise的初始状态 pending,以及参数执行函数 executor,并且按照规范设计了一个 resolve回调函数集合数组 onResolvedCallback以及 一个 reject回调函数集合数组onRejectedCallBack,那么构造函数的初始化就基本完成了。

  接下来需要在构造函数中完善 resolvereject两个函数,完善之后的代码如下。

function Promsie(executor) {
  this.status = "pending"; // Promise当前的状态
  this.data = undefined; // Promise的值
  this.onResolvedCallBack = []; // Promise resolve时的回调函数集
  this.onRejectedCallBack = []; // Promise reject时的回调函数集
  executor(resolve, reject); // 执行executor并传入相应的参数

  const resolve = (value) => {
    //
  };
  const reject = (value) => {
    //
  };
}

  resolvereject内部应该怎么实现呢?我们根据规范知道这两个方法主要做的事情就是返回对应状态的值 value或者 reason,并把 Promise内部的 statuspending变成对应的状态,并且这个状态在改变了之后是不可以逆转的。

function Promsie(executor) {
 //上面省略
  const resolve = (value) => {
    if (this.status === "pending") {
      this.status = "resolved";
      this.data = value;
      for (let i = 0; i < this.onResolvedCallBack.length; i++) {
        this.onResolvedCallBack[i](value);
      }
    }
  };
  const reject = (reason) => {
    if (this.status === 'pending') {
        this.status = 'rejected';
        this.data = reason;
        for (let i = 0;i<this.onRejectedCallBack.length;i++) {
            this.onRejectedCallBack[i](reason)
        }
    }
  };
}

  上述代码所展示的,基本就是在判断状态为 pending 之后,把状态改为相应的值,并把对应的 valuereason存在内部的 data属性上面,之后执行相应的回调函数。逻辑比较简单,无非是由于 onResolveCallbackonRejectedCallback这两个是数组,需要通过循环来执行。

实现 then 方法

  根据标准,我们要考虑几个问题: then方法是 Promise执行完之后可以拿到 value或者 reason的方法,并且还要保持 then执行之后,返回的依旧是一个 Promise方法,还要支持多次调用(上面标准中提到过)。

Promsie.prototype.then = function (onResolved,onRejected){
    const self = this;
    let promise2;
    // 根据标准,如果then的参数不是function,则忽略它  
    onResolved = typeof onResolved === "function" ? onResolved : function(v){};
    onRejected = typeof onRejected === 'function' ? onRejected : function(v){};
    if (self.status === 'resolved') {
        return promise2 = new Promsie(function(resolve,reject){

        });
    }
    if (self.status === 'rejected') {
        return promise2 = new Promise(function(resolve,reject){

        })
    }
    if (self.status === 'pending') {
        return promise2 = new Promsie(function(resolve,reject){
            
        })
    }
}

  在 then方法内部先初始化了 Promise2的对象,用来存放执行之后返回的 Promise,并且还需要判断 then方法传参进来的两个参数必须为函数,这样才可以继续执行。

  上面我只是搭建了 then方法框架的整体思路,但是不同状态的返回细节处理也需要完善,通过仔细阅读标准,完善之后的 then的代码如下。

Promsie.prototype.then = function (onResolved, onRejected) {
  let promise2;
  // 根据标准,如果then的参数不是function,则忽略它
  onResolved = typeof onResolved === "function" ? onResolved : function (v) {};
  onRejected = typeof onRejected === "function" ? onRejected : function (v) {};
  if (this.status === "resolved") {
    return (promise2 = new Promsie(function (resolve, reject) {
      // 如果promise1的状态已经确定并且是resolved,我们调用onResolved,考虑到有可能throw,所以还需要将其包在try/catch块里
      try {
        let x = onResolved(this.data);
        // 如果onResolved的返回值是一个Promise对象,直接取它的结果作为promise2的结果
        if (x instanceof Promsie) {
          x.then(resolve, reject);
        }
        resolve(x); //否则,以它的返回值作为promise2的结果
      } catch (e) {
        reject(e); //如果出错,以捕获到的错误作为promise2的结果
      }
    }));
  }
  if (this.status === "rejected") {
    return (promise2 = new Promise(function (resolve, reject) {
      // 此处与前一个if块的逻辑几乎相同,区别在于所调用的是onRejected函数
      try {
        let x = onRejected(this.data);
        if (x instanceof Promsie) {
          x.then(resolve, reject);
        }
      } catch (e) {
        reject(e);
      }
    }));
  }
  if (this.status === "pending") {
    return (promise2 = new Promsie(function (resolve, reject) {
      // 如果当前的Promise还处于pending状态,我们并不能确定调用onResolved还是onRejected,只能等到Promise的状态确定后,才能确定如何处理
      this.onResolvedCallBack.push((resolve,reject)=>{
        try { 
            let x = onResolved(this.data);
            if (x instanceof Promise) {
                x.then(resolve,reject);
            }
        }catch (e) {
            reject(e);
        }
      })
      this.onRejectedCallBack.push((resolve,reject)=>{
          try {
              let x = onRejected(this.data);
              if (x instanceof Promsie) {
                  x.then(resolve,reject);
              }
          } catch (e) {
              reject(e);
          }
      })
    }));
  }
};

  根据上面的代码可以看出,我们基本实现了一个符合标准的 then方法。但是标准里提到了,还要支持不同的 Promise进行交互,关于不同的 Promise交互其实Promise 标准说明中有提到。其中详细指定了如何通过 then的实参返回的值来决定 Promise2的状态。

  关于为何需要不同的 Promise实现交互,原因应该是 Promise并不是 JS一开始存在的标准,如果你使用的某一个库中封装了一个 Promise的实现,想象一下如果它不能跟你自己使用的 Promise实现交互的情况,其实还是会有问题的,因此我们还需要调整一下 then方法中执行 Promise的方法。

  另外还有一个需要注意的是,在 Promise/A+ 规范中,onResolvedonRejected这两项函数需要异步调用。

  所以我们需要对代码做一点变动,即在处理 Promise进行 resolve或者 reject的时候,加上 setTimeout(fn, 0)

下面结合上面两点调整,给出完整版的代码

try {
  module.exports = Promise;
} catch (e) {}

function Promise(executor) {
  this.status = "pending";
  this.onResolvedCallback = [];
  this.onRejectedCallback = [];
  const resolve = (value) => {
    if (value instanceof Promise) {
      return value.then(resolve, reject);
    }
    setTimeout(() => {
      // 异步执行所有的回调函数
      if (this.status === "pending") {
        this.status = "resolved";
        this.data = value;
        for (let i = 0; i < this.onResolvedCallback.length; i++) {
          this.onResolvedCallback[i](value);
        }
      }
    });
  };
  const reject = (reason) => {
    setTimeout(() => {
      // 异步执行所有的回调函数
      if (this.status === "pending") {
        this.status = "rejected";
        this.data = reason;
        for (let i = 0; i < this.onRejectedCallback.length; i++) {
          this.onRejectedCallback[i](reason);
        }
      }
    });
  };
  try {
    executor(resolve, reject);
  } catch (reason) {
    reject(reason);
  }
}
function resolvePromise(promise2, x, resolve, reject) {
  let then;
  let thenCalledOrThrow = false;
  if (promise2 === x) {
    return reject(new TypeError("Chaining cycle detected for promise!"));
  }
  if (x instanceof Promise) {
    if (x.status === "pending") {
      x.then((v) => {
        resolvePromise(promise2, v, resolve, reject);
      }, reject);
    } else {
      x.then(resolve, reject);
    }
    return;
  }
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    try {
      then = x.then;
      if (typeof then === "function") {
        then.call(
          x,
          (rs = (y) => {
            if (thenCalledOrThrow) return;
            thenCalledOrThrow = true;
            return resolvePromise(promise2, y, resolve, reject);
          }),
          (rj = (r) => {
            if (thenCalledOrThrow) return;
            thenCalledOrThrow = true;
            return reject(r);
          })
        );
      } else {
        resolve(x);
      }
    } catch (e) {
      if (thenCalledOrThrow) return;
      thenCalledOrThrow = true;
      return reject(e);
    }
  } else {
    resolve(x);
  }
}

Promise.prototype.then = function (onResolved, onRejected) {
  let promise2;
  onResolved =
    typeof onResolved === "function"
      ? onResolved
      : function (v) {
          return v;
        };

  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : function (r) {
          throw r;
        };

  if (this.status === "resolved") {
    return (promise2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        // 异步执行onResolved
        try {
          let x = onResolved(this.data);
          resolvePromise(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }
  if (this.status === "rejected") {
    return (promise2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        // 异步执行onRejected
        try {
          let x = onRejected(this.data);
          resolvePromise(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }
  if (this.status === "pending") {
    // 这里之所以没有异步执行,是因为这些函数必然会被resolve或reject调用,而resolve或reject函数里的内容已是异步执行,构造函数里的定义
    return (promise2 = new Promise((resolve, reject) => {
      this.onResolvedCallback.push((value) => {
        try {
          let x = onResolved(value);
          resolvePromise(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
      this.onRejectedCallback.push((reason) => {
        try {
          let x = onRejected(reason);
          resolvePromise(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
    }));
  }
};

Promise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected);
};

// 最后这个是测试用的,后面会说

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

    dfd.reject = reject;
  });

  return dfd;
};

  上面这段代码就是通过一步步优化调整出来的最终版,其中细节也是比较多的,介于篇幅问题,暂时能讲的点就先说这么多。如果你还有哪里不清楚的,最好还是动手实践去理解。

  最终版的 Promise 的实现还是需要经过规范的测试(Promise /A+ 规范测试的工具地址为:https://github.com/promises-aplus/promises-tests),需要暴露一个 deferred方法(即 exports.deferred 方法),上面提供的代码中我已经将其加了进去。

最后,执行如下代码 npm安装之后,即可执行测试。

npm i -g promises-aplus-tests
promises-aplus-tests Promise.js

总结

  到这里,可以再思考一下 Promise /A+规范中的一些细节,以及实现过程中需要注意的问题,如果能够在面试中实现一个这样的 Promise,就基本可以满足岗位的需求了。

  通过本次的学习,应该能理解 Promise底层的实现逻辑了,虽然并不一定有场景适合落地业务,但是整个手动实现的过程对于JS 编码能力的提升会很有帮助。

你可能感兴趣的:(JavaScript,核心原理精解,javascript)