手寫實現知足 Promise/A+ 規範的 Promise

最近看了 Promise/A+ 的規範,嘗試實現了一個知足 promises-aplus-tests 測試的 Promise 類,在實現規範的過程當中,對於 Promise 自己也加深了理解,這篇文章就將個人實現過程分享出來。git

  • 本文的代碼倉庫在這裏,歡迎 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.github

  1. thenable 對象是一類具備 then 方法的對象或者函數。

1.2 「thenable」 is an object or function that defines a then method.npm

  1. 每一個 Promise 內部都有一個 value 值,這個 value 值能夠是任意合法的 JavaScript 數據類型。

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

  1. 除了 value 屬性,Promise 內部還有一個 reason 屬性,用來存放 Promise 狀態變爲 rejected 的緣由

1.5 「reason」 is a value that indicates why a promise was rejected.瀏覽器

構造 MyPromise 類

根據上面的介紹,能夠初步構造一個 MyPromise 類:bash

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 值。 如今,咱們能夠對上面的代碼進行一些測試:frontend

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 方法。dom

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

then 方法

then 方法接受兩個參數:Promise 狀態轉換爲 fulfilled 的回調(成功回調)和狀態轉換爲 rejected 的回調(失敗回調),這兩個回調函數是可選的。async

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~。

完。

相關文章
相關標籤/搜索