Promise的實現原理

310 阅读7分钟

  本文通过实现一个Promise对象,深入了解Promise的运行原理。 首先我们正式开始写代码,实现Promise實例前,思考一下当我们创建Promise實例时,JS到底做了什么。 举个例子:

let p = new Promise((resolve,reject) =>{
  let a = 3 + 1;
  a == 4 ? resolve(a) : reject(a);
})

  我们创建一个Promise實例,里面需要放一个执行函数,而且函数必须要有resolve和reject的参数,用来保存成功的值或失败的原因以及修改状态。对象一创建,Promise的构造函数就运行传入的函数。上述例子,对象一创建,执行Promise实例存放的函数,然后调用resolve,把Promise的状态改为 fulfilled把成功的值存入,也就是4。好啦,现在我们可以开始写一个对象,叫MyPromise:

const PENDING = 'pending'; // 等待
const FULFILLED = 'fulfilled'; // 成功
const REJECTED = 'rejected'; // 失败

class MyPromise {
  constructor(executor){
    try {
    executor(this.resolve, this.reject);
    } catch(e) {
      This.reject(e)
    }
  }
  status = PENDING;
  value = undefined;
  reason = undefined;

  resolve => value => {
    if(this.status !== PENDING) return;
    this.status = FULFILLED;
    this.value = value}

  reject => reason => {
    if(this.status !== PENDING) return;
    this.status = REJECTED;
    this.reason = reason}
}

  现在把一系列成员变量和常量定义好,也把构造函数写好了。其中,status是用来保存Promise的状态的,value用来保存成功的值,reason则是保存失败的原因,然后定义resolve和reject函数,它们分别改变status的值,而且判断如果status不为pending则退出,最后存入成功的值或失败的原因。

  把构造函数和一系列成员变量写好后,现在我们来看Promise里最关键的方法 -- then。then里面有两个回调函数,根据状态而去调用,回调函数里保含Promise传来的值。例如:

let p = new Promise((resolve,reject) =>{
  let a = 3 + 1;
  a == 4 ? resolve(a) : reject(a);
})
p.then(res => console.log(res)) //結果是 4

  then最后会传出一个新的Promise實例。现在我们来想想要怎么写。先不要去想具体内容,想一下大概形式是怎样。首先肯定要有两个参数作为成功和失败的回调函数,我们不妨叫它们作 successCallback, failCallback,然后最后结果一定是传出一个Promise實例。好,现在我们用代码来表示:

then (successCallback, failCallback) {
  ...  //內容
  let promise = new MyPromise((resolve, reject) => {
    ...
  })
  return promise
}

大概就是如上述般的写法。

 接下来我们要思考到底then方法一开始是做什么的。它是要接收回调函数,如果放上非回调函数,则会直接在新的Promise中放入前一个Promise对象传入的值,即可理解为:  

then(3) //等於 then(value => return Promise.resolve(value));

所以then会先判断传入的参数是否为回调函数,再作相应处理。写成代码形式:

  then (successCallback, failCallback) {
    successCallback = successCallback ? successCallback : value => value;
    failCallback = failCallback ? failCallback: reason => { throw reason };
    ...
 }

如果不是函数,则生成传入成功的值的函数或抛出失败原因的函数。接着就要生成一个新的Promise實例,执行函数根据前一个Promise的状态来决定调用的函数。这里先把成功与失败的处理写出来。

then (successCallback, failCallback) {
  ...
  let promise = new MyPromise((resolve, reject) => {
   if (this.status === FULFILLED) {
     try {
       let x = successCallback(this.value);
     }catch (e) {
       reject(e);
     }
   }else if (this.status === REJECTED) {
      try {
        let x = failCallback(this.reason);
      }catch (e) {
         reject(e);
      }
    }
  });
   return promise;

  别忘了,then里面的回调函数是要放入微任务队列,因此我们需要调用 queueMicrotask,因此代码应写为:

...
    let promise = new MyPromise((resolve, reject) => {
      // 判断状态
      if (this.status === FULFILLED) {
        queueMicrotask(() => {
          try {
            let x = successCallback(this.value);
          }catch (e) {
            reject(e);
          }
        })
      }else if (this.status === REJECTED) {
        queueMicrotask(() => {
          try {
            let x = failCallback(this.reason);
          }catch (e) {
            reject(e);
          }
        })
      }   ...
   return promise

  好啦,现在then方法开始像样啦,但我们还需要去resolve或reject,这样这个Promise实例的状态才可确认下来。大可以把回调函数传回来的值直接resolve或reject,然而我们不知道传来的是什么,如果传来的是Promise自身,则会出现无穷循环,这意味着在resolve或reject之前,我们需要判断一下传来的值。我们可以写一个私有方法,在JS写私有方法,可以在class外面写一个函数 (如果是typescript,可以如面向对象语言,直接用私有方法)。

function resolvePromise (promise, x, resolve, reject) {
  if (promise === x) { 
   return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  if (x instanceof MyPromise) {
    // promise 对象
    // x.then(value => resolve(value), reason => reject(reason));
    x.then(resolve, reject);
  } else {
    // 普通值
    resolve(x);
  }}

  先判断传来的值是否与所创建的 Promise 实例相同,如果相同,则报错。如果传来的是值,直接调用promise实例的resolve。若为 Promise实例,则直接调用该实例的then,把resolve和reject传入,作回调函数。注意,这里的resolve和reject是 promise 实例的,不是回调函数传出的Promise实例。在我们把这个私有方法放入then方法前,我们考虑一下最后一个状态--pending。

  如果Promise的状态是pending,代码还没结束,所以还得运行下来,但由于状态还没有确定下来,只能先把then的回调函数存起来,等到执行函数运行完,确定状态,才把相应的回调函数提出来执行。因此我们需要重构一下Promise,增加两个数组,用来保存成功和失败的回调函数,然后resolve和reject确定相应数组有没有回调函数,如果有,则调用。相应的代码如下:

  successCallback = [];
  failCallback = [];
  resolve = value => {
    if (this.status !== PENDING) return;
    this.status = FULFILLED;
    this.value = value;
    while(this.successCallback.length) this.successCallback.shift()()  }
  reject = reason => {
    if (this.status !== PENDING) return;
    this.status = REJECTED;
    this.reason = reason;
    while(this.failCallback.length) this.failCallback.shift()()  }

  新增两个数组,分别存入成功和失败的回调函数,resolve和reject通过先进先出的方式调出函数。

  在then方法中,加上处理pending 相应的代码,调用存储函数的数组把回调函数保存下来:

        this.successCallback.push(() => {
          queueMicrotask(() => {
            try {
              let x = successCallback(this.value);
              resolvePromise(promise, x, resolve, reject)
            }catch (e) {
              reject(e);
            }
          })
        }); 
       this.failCallback.push(() => {
          queueMicrotask(() => {
            try {
              let x = failCallback(this.reason);
              resolvePromise(promise, x, resolve, reject)
            }catch (e) {
              reject(e);
            }
          })
        });

  現在我們完成then方法了。完整代碼如下:

  then (successCallback, failCallback) { 
   successCallback = successCallback ? successCallback : value => value;
    failCallback = failCallback ? failCallback: reason => { throw reason };
    let promise = new MyPromise((resolve, reject) => {
      if (this.status === FULFILLED) {
        queueMicrotask(() => {
          try {
            let x = successCallback(this.value);
            resolvePromise(promise, x, resolve, reject)
          }catch (e) {
            reject(e);
          }
        })
      }else if (this.status === REJECTED) {
        queueMicrotask(() => { 
         try {
            let x = failCallback(this.reason);
            resolvePromise(promise, x, resolve, reject)
          }catch (e) {
            reject(e);
          }
        })
      } else {
        this.successCallback.push(() => {
          queueMicrotask(() => {
            try {
              let x = successCallback(this.value);
              resolvePromise(promise, x, resolve, reject)
            }catch (e) {
              reject(e);
            }
          })
        });
        this.failCallback.push(() => {
          queueMicrotask(() => {
            try {
              let x = failCallback(this.reason);
              resolvePromise(promise, x, resolve, reject)
            }catch (e) {
              reject(e);
            }
          })
        });
      }
    });
    return promise;  }

  基本上,到了这里,Promise的实现完成大半,后面实现其馀的方法吧。

  先说catch方法。catch负责调用失败回调函数,把之前传出的失败原因作为参数放进去。因此,代码可以这样写:

  catch (failCallback) {
    return this.then(undefined, failCallback)  }

  其实就是调用then,不过没有成功的回调函数。 

  resolve是一个静态方法,返回一个经resolve的Promise实例,即该Promise的状态是 fulfilled。

  static resolve (value) {
    if (value instanceof MyPromise) return value;
    return new MyPromise(resolve => resolve(value));  }}

  根据传入的参数决定过执行过程。若为Promise实例,直接返回,否则,把参数放入一个新的Promise实例。

  finally方法是不管成功或失敗,都必须调用传入的回调函数,返回一个新的Promise实例 。 

  finally (callback) {
    return this.then(value => {
      return MyPromise.resolve(callback()).then(() => value);
    }, reason => {
      return MyPromise.resolve(callback()).then(() => { throw reason })
    })  }

  all方法可以接受一个数组作为参数,传出一个新的Promise实例,里面有一个结果数组,把相应结果作为Promise的成功的值。如果数组里的元素为值,直接返回,作为结果。若为Promise,根据它的执行函数结果进行相应处理,若成功,放入结果数组,否则直接调用其失败函数,因此不会调用新的 Promise的resolve。

  static all (array) {
    let result = [];
    let index = 0;
    return new MyPromise((resolve, reject) => {
      function addData (key, value) {
        result[key] = value;
        index++;
        if (index === array.length) {
          resolve(result);
        }
      }
      for (let i = 0; i < array.length; i++) {
        let current = array[i];
        if (current instanceof MyPromise) {
          // promise 对象
          current.then(value => addData(i, value), reason => reject(reason))
        }else {
          // 普通值
          addData(i, array[i]);
        }
      }
    })
  }

  基本上Promise 的方法已经实现了,剩下的我放在后面附上的完整代码。 

  我们通过实现Promise,了解它的执行原理,而且在不知不觉间了解函数式编程。函数式编程,就是指用纯函数方式来编程。简单来说,纯函数就是指函数输入什么参数,输出结果不变,不管执行多少次,结果依然不变。Promise的实现也体现了该思想。当创建一个新的Promise实例,构造函数执行传入的函数,决定了Promise的状态,确定了成功的值和失败的值。构建后,实例的成员变量已经确定,之后不管调用任何方法,也不会改变里面的变量。 

  这样做有什么好处?好处就是保持不变性,调用一个实例的方法,其结果是预料的,方便测试。

  最後附上完整代碼:

codepen.io/dominguitol…