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

Charleylla發表於2018-12-07

最近看了 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~。

完。

相關文章