手写实现满足 Promise/A+ 规范的 Promise

1,253 阅读11分钟

最近看了 Promise/A+ 的规范,尝试实现了一个满足 promises-aplus-tests 测试的 Promise 类,在实现规范的过程中,对于 Promise 本身也加深了理解,这篇文章就将我的实现过程分享出来。

  • 本文的代码仓库在这里,欢迎 Star~。

前置知识

  1. Promise 是用来解决异步问题的一个方案,相当于异步操作的占位符。
  2. 每个 Promise 只有三种状态:pendingfulfilledrejected,状态只能从 pending 转移到 fulfilled 或者 rejected,一旦状态变成fulfilled 或者 rejected,就不能再更改其状态。

2.1.1 When pending, a promise: 2.1.1.1 may transition to either the fulfilled or rejected state. 2.1.2 When fulfilled, a promise: 2.1.2.1 must not transition to any other state. 2.1.2.2 must have a value, which must not change. 2.1.3 When rejected, a promise: 2.1.3.1 must not transition to any other state. 2.1.3.2 must have a reason, which must not change.

  1. thenable 对象是一类具有 then 方法的对象或者函数。

1.2 “thenable” is an object or function that defines a then method.

  1. 每个 Promise 内部都有一个 value 值,这个 value 值可以是任意合法的 JavaScript 数据类型。

1.3 “value” is any legal JavaScript value (including undefined, a thenable, or a promise).

  1. 除了 value 属性,Promise 内部还有一个 reason 属性,用来存放 Promise 状态变为 rejected 的原因

1.5 “reason” is a value that indicates why a promise was rejected.

构造 MyPromise 类

根据上面的介绍,可以初步构造一个 MyPromise 类:

class MyPromise {
  constructor(exector) {
    this.status = MyPromise.PENDING;
    this.value = null;
    this.reason = null;
    this.initBind();
    this.init(exector);
  }
  initBind() {
    // 绑定 this
    // 因为 resolve 和 reject 会在 exector 作用域中执行,因此这里需要将 this 绑定到当前的实例
    this.resolve = this.resolve.bind(this);
    this.reject = this.reject.bind(this);
  }
  init(exector) {
    try {
      exector(this.resolve, this.reject);
    } catch (err) {
      this.reject(err);
    }
  }
  resolve(value) {
    if (this.status === MyPromise.PENDING) {
      this.status = MyPromise.FULFILLED;
      this.value = value;
    }
  }
  reject(reason) {
    if (this.status === MyPromise.PENDING) {
      this.status = MyPromise.REJECTED;
      this.reason = reason;
    }
  }
}

// 2.1 A promise must be in one of three states: pending, fulfilled, or rejected.
MyPromise.PENDING = "pending"
MyPromise.FULFILLED = "fulfilled"
MyPromise.REJECTED = "rejected"

exector 是创建 Promise 对象时传递给构造函数的参数,resolvereject 方法分别用来将 Promise 对象的状态由 pending 转换成 fulfilledrejected,并向 Promise 对象中写入相应的 value 或者 reason 值。 现在,我们可以对上面的代码进行一些测试:

const p1 = new MyPromise((resolve,reject) => {
  const rand = Math.random();
  if(rand > 0.5) resolve(rand)
  else reject(rand)
})
console.log(p1)
// MyPromise {status: "fulfilled", value: 0.9121690746412516, reason: null, resolve: ƒ, reject: ƒ}

上面的代码,已经可以让 Promise 对象实现状态变换,并保存 value 或者 reason 值,但单纯完成状态的转换和保存值是不够的,作为异步的解决方案,我们还需要让 Promise 对象在状态变换后再做点什么。 这就需要我们为 Promise 对象再提供一个 then 方法。

A promise must provide a then method to access its current or eventual value or reason.

then 方法

then 方法接受两个参数:Promise 状态转换为 fulfilled 的回调(成功回调)和状态转换为 rejected 的回调(失败回调),这两个回调函数是可选的。

A promise’s then method accepts two arguments: promise.then(onFulfilled, onRejected) 2.2.1 Both onFulfilled and onRejected are optional arguments: 2.2.1.1 If onFulfilled is not a function, it must be ignored. 2.2.1.2 If onRejected is not a function, it must be ignored.

下面为 MyPromise 类添加一个 then 方法:

...
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : () => {}
    onRejected = typeof onRejected === "function" ? onRejected : () => {}
    if (this.status === MyPromise.FULFILLED) {
      try{
        onFulfilled(this.value)
      }catch(e){
        onRejected(e)
      }
    }

    if (this.status === MyPromise.REJECTED) {
      try{
        onRejected(this.reason);
      }catch(e){
        onRejected(e)
      }
    }
  }
...

下面测试一下 then 方法:

const p1 = new MyPromise((resolve) => resolve("Success"))
p1.then(data => console.log(data))
// Success

这里,我们初步完成了 MyPromise 类的 then 方法。 但仔细看上面的 then 方法和 MyPromise 类的实现,还存在几个缺陷:

  1. 只处理了状态为 fulfilledrejected 的情况,没有处理状态为 pending 的情况
  2. onFulfilledonRejected 方法是同步执行的,也就是说,调用 then 方法,就会执行 onFulfilledonRejected 方法
  3. MyPromise 类中的 resolvereject 方法也是同步的,这意味着会出现下面的情况:
console.log("START")
const p2 = new MyPromise(resolve => resolve("RESOLVED"))
console.log(p2.value)
console.log("END")

输出结果为:

START
RESOLVED
END

按照规范,Promise 应该是异步的。

2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

规范还指出了,应该使用 setTimeoutsetImmediate 这样的宏任务方式,或者 MutationObserverprocess.nextTick 这样的微任务方式,来调用 onFulfilledonRejected 方法。

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver  or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

  1. MyPromise 对象的状态不能被异步的改变,换句话说,无法满足 exector 方法为异步的情况:
const p3 = new MyPromise(resolve => setTimeout(() => resolve("RESOLVED")));
p3.then(data => console.log(data))
// 无输出

这里无输出的原因是在实现 then 方法的时候,没有处理状态为 pending 的情况,那么在 pending 状态下,对于 then 方法的调用,不会有任何的响应,因此在 then 方法中,对于 pending 状态的处理也很重要。 下面就针对上面出现的问题,做一些改进。

改进

首先,应该确保 onFulfilledonRejected 方法,以及 resolvereject 方法是异步调用的:

...
  resolve(value) {
    if (this.status === MyPromise.PENDING) {
      setTimeout(() => {
        this.status = MyPromise.FULFILLED;
        this.value = value;
        this.onFulfilledCallback.forEach(cb => cb(this.value));
      })
    }
  }

  reject(reason) {
    if (this.status === MyPromise.PENDING) {
      setTimeout(() => {
        this.status = MyPromise.REJECTED;
        this.reason = reason;
        this.onRejectedCallback.forEach(cb => cb(this.reason));
      })
    }
  }

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : () => {}
    onRejected = typeof onRejected === "function" ? onRejected : () => {}
    if (this.status === MyPromise.FULFILLED) {
      setTimeout(() => {      
        try{
          onFulfilled(this.value)
        }catch(e){
          onRejected(e)
        }
      })
    }

    if (this.status === MyPromise.REJECTED) {
      setTimeout(() => {      
        try{
          onRejected(this.reason);
        }catch(e){
          onRejected(e)
        }
      })
    }
  }
...

然后,需要在 MyPromise 类中,在设置两个队列:onFulfilledCallback,和 onRejectedCallback,用来存放在 pending 状态下,调用 then 方法时传入的回调函数。 在调用 resolvereject 方法时,需要将队列中存放的回调按照先后顺序依次调用(是不是感觉很像浏览器的事件环机制)。

class MyPromise {
  constructor(exector) {
    this.status = MyPromise.PENDING;
    this.value = null;
    this.reason = null;

    /**
     * 2.2.6 then may be called multiple times on the same promise
     *  2.2.6.1 If/when promise is fulfilled, all respective onFulfilled callbacks must execute in the order of their originating calls to then
     *  2.2.6.2 If/when promise is rejected, all respective onRejected callbacks must execute in the order of their originating calls to then.
     */

    this.onFulfilledCallback = [];
    this.onRejectedCallback = [];
    this.initBind();
    this.init(exector);
  }
  initBind() {
    // 绑定 this
    // 因为 resolve 和 reject 会在 exector 作用域中执行,因此这里需要将 this 绑定到当前的实例
    this.resolve = this.resolve.bind(this);
    this.reject = this.reject.bind(this);
  }
  init(exector) {
    try {
      exector(this.resolve, this.reject);
    } catch (err) {
      this.reject(err);
    }
  }

  resolve(value) {
    if (this.status === MyPromise.PENDING) {
      setTimeout(() => {
        this.status = MyPromise.FULFILLED;
        this.value = value;
        this.onFulfilledCallback.forEach(cb => cb(this.value));
      })
    }
  }

  reject(reason) {
    if (this.status === MyPromise.PENDING) {
      setTimeout(() => {
        this.status = MyPromise.REJECTED;
        this.reason = reason;
        this.onRejectedCallback.forEach(cb => cb(this.reason));
      })
    }
  }

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : () => {}
    onRejected = typeof onRejected === "function" ? onRejected : () => {}
    if (this.status === MyPromise.FULFILLED) {
      setTimeout(() => {      
        try{
          onFulfilled(this.value)
        }catch(e){
          onRejected(e)
        }
      })
    }

    if (this.status === MyPromise.REJECTED) {
      setTimeout(() => {      
        try{
          onRejected(this.reason);
        }catch(e){
          onRejected(e)
        }
      })
    }

    if (this.status === MyPromise.PENDING) {
      // 向对了中装入 onFulfilled 和 onRejected 函数
      this.onFulfilledCallback.push((value) => {
        try{
          onFulfilled(value)
        }catch(e){
          onRejected(e)
        }
      })

      this.onRejectedCallback.push((reason) => {
        try{
          onRejected(reason)
        }catch(e){
          onRejected(e)
        }
      })
    }
  }
}

// 2.1 A promise must be in one of three states: pending, fulfilled, or rejected.
MyPromise.PENDING = "pending"
MyPromise.FULFILLED = "fulfilled"
MyPromise.REJECTED = "rejected"

进行一些测试:

console.log("===START===")
const p4 = new MyPromise(resolve => setTimeout(() => resolve("RESOLVED")));
p4.then(data => console.log(1,data))
p4.then(data => console.log(2,data))
p4.then(data => console.log(3,data))
console.log("===END===")

输出结果:

===START===
===END===
1 'RESOLVED'
2 'RESOLVED'
3 'RESOLVED'

实现链式调用

规范还规定,then 方法必须返回一个新的 Promise 对象,以实现链式调用。

2.2.7 then must return a promise. promise2 = promise1.then(onFulfilled, onRejected);

如果 onFulfilledonRejected 是函数,就用函数调用的返回值,来改变新返回的 promise2 对象的状态。

2.2.7.1 If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x). 2.2.7.2 If either onFulfilled or onRejected throws an exception epromise2 must be rejected with e as the reason.

这里提到的 Promise Resolution Procedure,其实是针对 onFulfilledonRejected 方法不同返回值的情况,来对 promise2 的状态来统一进行处理,我们暂时先忽略,后文再提供实现。

另外,如果 onFulfilledonRejected 不是函数,那么就根据当前 promise 对象(promise1)的状态,来改变 promise2 的状态。

2.2.7.3 If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1. 2.2.7.4 If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.

由于在前面的代码,已对 onFulfilledonRejected 函数进行来处理,如果不是函数的话,提供一个默认值:

onFulfilled = typeof onFulfilled === "function" ? onFulfilled : () => {}
onRejected = typeof onRejected === "function" ? onRejected : () => {}

并且每次调用 onFulfilledonRejected 方法时,都会传入当前实例的 value 或者 reason 属性,因此对于 onFulfilledonRejected 不是函数的特殊情况,直接将传给它们的参数返回即可,promise2 依旧使用 onFulfilledonRejected 的返回值来改变状态:

onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason }

上面的方案,还顺带解决了值穿透的问题。所谓值穿透,就是调用 then 方法时,如果不传入参数,下层链条中的 then 方法还能够正常的获取到 value 或者 reason 值。

new MyPromise(resolve => setTimeout(() => { resolve("Success") }))
.then()
.then()
.then()
...
.then(data => console.log(data));

下面就根据上面的陈述,对 then 方法做进一步的改进:

···
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
    onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason }
    let promise2;
    if (this.status === MyPromise.FULFILLED) {
      return promise2 = new MyPromise((resolve,reject) => {
        setTimeout(() => {      
          try{
            const x = onFulfilled(this.value)
            resolve(x);
          }catch(e){
            reject(e)
          }
        })
      })
    }

    if (this.status === MyPromise.REJECTED) {
      return promise2 = new MyPromise((resolve,reject) => {
        setTimeout(() => {      
          try{
            const x = onRejected(this.reason)
            resolve(x);
          }catch(e){
            reject(e)
          }
        })
      })
    }

    if (this.status === MyPromise.PENDING) {
      return promise2 = new MyPromise((resolve,reject) => {
        // 向对了中装入 onFulfilled 和 onRejected 函数
        this.onFulfilledCallback.push((value) => {
          try{
            const x = onFulfilled(value)
            resolve(x)
          }catch(e){
            reject(e)
          }
        })

        this.onRejectedCallback.push((reason) => {
          try{
            const x = onRejected(reason)
            resolve(x);
          }catch(e){
            reject(e)
          }
        })
      })
    }
  }
···

规范规定,then 方法必须返回一个新的 Promise 对象(promise2),新的 promise2 的状态必须依赖于调用 then 方法的 Promise 对象(promise1)的状态,也就是说,必须要等到 promise1 的状态变成 fulfilled 或者 rejected 之后,promise2 的状态才能进行改变。 因此,在 then 方法的实现中,在当前的 Promise 对象(promise1)的状态为 pending 时,将改变 promise2 状态的方法加入到回调函数的队列中。

实现 resolvePromise 方法

上面的代码,处理了 onFulfilledonRejected 方法的返回值的情况,以及实现了 then 方法的链式调用。 现在考虑一个问题,如果 onFulfilledonRejected 方法返回的是一个 Promise 对象,或者是具有 then 方法的其他对象(thenable 对象),该怎么处理呢? 规范中提到,对于 onFulfilledonRejected 的返回值的,提供一个 Promise Resolution Procedure 方法进行统一的处理,以适应不同的返回值类型。

2.2.7.1 If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x).

我们将这个方法命名为 resolvePromise 方法,将其设计为 MyPromise 类上的一个静态方法。 resolvePromise 静态方法的作用,就是根据 onFulfilledonRejected 不同的返回值(x)的情况,来改变 then 方法返回的 Promise 对象的状态。 可以这样理解:我们将改变 promise2 对象的状态的过程,移动到了 resolvePromise 方法中,以便处理更多的细节问题。 下面是 resolvePromise 方法的实现:

MyPromise.resolvePromise = (promise2,x,resolve,reject) => {
  let called = false;
  /**
   * 2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
   */
  if(promise2 === x){
    return reject(new TypeError("cannot return the same promise object from onfulfilled or on rejected callback."))
  }
  
  if(x instanceof MyPromise){
    // 处理返回值是 Promise 对象的情况
    /**
     * new MyPromise(resolve => {
     *  resolve("Success")
     * }).then(data => {
     *  return new MyPromise(resolve => {
     *    resolve("Success2")
     *  })
     * })
     */
    if(x.status === MyPromise.PENDING){
      /**
       * 2.3.2.1 If x is pending, promise must remain pending until x is fulfilled or rejected.
       */
      x.then(y => {
        // 用 x 的 fulfilled 后的 value 值 y,去设置 promise2 的状态
        // 上面的注视,展示了返回 Promise 对象的情况,这里调用 then 方法的原因
        // 就是通过参数 y 或者 reason,获取到 x 中的 value/reason

        // 拿到 y 的值后,使用 y 的值来改变 promise2 的状态
        // 依照上例,上面生成的 Promise 对象,其 value 应该是 Success2

        // 这个 y 值,也有可能是新的 Promise,因此要递归的进行解析,例如下面这种情况

        /**
         * new Promise(resolve => {
         *  resolve("Success")
         * }).then(data => {
         *  return new Promise(resolve => {
         *    resolve(new Promise(resolve => {
         *      resolve("Success3")
         *    }))
         *  })
         * }).then(data => console.log(data))
         */

        //  总之,使用 “return”链中最后一个 Promise 对象的状态,来决定 promise2 的状态

        MyPromise.resolvePromise(promise2, y, resolve, reject)
      },reason => {
        reject(reason)
      })
    }else{
      /**
       * 2.3 If x is a thenable, it attempts to make promise adopt the state of x, 
       * under the assumption that x behaves at least somewhat like a promise. 
       * 
       * 2.3.2 If x is a promise, adopt its state [3.4]:
       * 2.3.2.2 If/when x is fulfilled, fulfill promise with the same value.
       * 2.3.2.4 If/when x is rejected, reject promise with the same reason.
       */
      x.then(resolve,reject)
    }
    /**
     * 2.3.3 Otherwise, if x is an object or function,
     */
  }else if((x !== null && typeof x === "object") || typeof x === "function"){
    /**
     * 2.3.3.1 Let then be x.then. 
     * 2.3.3.2 If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
     */
    try{
      // then 方法可能设置了访问限制(setter),因此这里进行了错误捕获处理
      const then = x.then;
      if(typeof then === "function"){

        /**
         * 2.3.3.2 If retrieving the property x.then results in a thrown exception e, 
         * reject promise with e as the reason.
         */

        /**
         * 2.3.3.3.1 If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
         * 2.3.3.3.2 If/when rejectPromise is called with a reason r, reject promise with r.
         */
        
        then.call(x,y => {
          /**
           * If both resolvePromise and rejectPromise are called, 
           * or multiple calls to the same argument are made, 
           * the first call takes precedence, and any further calls are ignored.
           */
          if(called) return;
          called = true;
          MyPromise.resolvePromise(promise2, y, resolve, reject)          
        },r => {
          if(called) return;
          called = true;
          reject(r);
        })
      }else{
        resolve(x)
      }
    }catch(e){
      /**
       * 2.3.3.3.4 If calling then throws an exception e,
       * 2.3.3.3.4.1 If resolvePromise or rejectPromise have been called, ignore it.
       * 2.3.3.3.4.2 Otherwise, reject promise with e as the reason.
       */

      if(called) return;
      called = true;
      reject(e)
    }
  }else{
    // If x is not an object or function, fulfill promise with x.
    resolve(x);
  }
}

在我实现规范的规程中,这个 resolvePromise 最最难理解的,主要是 return 链这里,因为想不到具体的场景。我将具体的场景通过注释的方式写在上面的代码中了,同样迷惑的童鞋可以看看。

进行 promises-aplus-tests 测试

通过 promises-aplus-tests 可以测试我们实现的 Promise 类是否满足 Promise/A+ 规范。 进行测试之前,需要为 promises-aplus-tests 提供一个 deferred 的钩子:

MyPromise.deferred  = function() {
  const defer = {}
  defer.promise = new MyPromise((resolve, reject) => {
    defer.resolve = resolve
    defer.reject = reject
  })
  return defer
}

try {
  module.exports = MyPromise
} catch (e) {
}

安装并运行测试:

npm install promises-aplus-tests -D
npx promises-aplus-tests promise.js

测试结果如下,全部通过:

测试结果.png
至此,我们实现了一个完全满足 Promise/A+ 规范的 Promise,本文的代码仓库在这里,欢迎 Star~。

完。