1. 程式人生 > 其它 >實現 Promise 要講武德——手把手實現一個遵循規範的 Promise

實現 Promise 要講武德——手把手實現一個遵循規範的 Promise

技術標籤:javascript前端jsjavascript

  一步步實現一個 promiseA+ 規範的promise。

Promises/A+ 規範

  Promise 是一個解決js非同步呼叫的方式,它於 ES6 以物件的形式實現和出現。

  在 Promise 解決js非同步呼叫的解決思路出現以後,Promise 標準出現以前,github上大大小小的 Promise 庫出現了上千個,實現思路和呼叫方式各不相同,質量也參差不齊。各個類庫經常出現引用了三四個 Promise 庫功能互相間還不能互通相容的情況。為了解決這個情況,社群出了一個開源的通用的 Promise 實現標準,promiseA+

Promises/A+ 規範規定了實現 Promise 三個核心點:

  • promise 的狀態
  • then方法
  • Resolution方法

  這三個核心點規範,使得所有的 Promise 類庫能夠通過 thenable 的物件進行互相之間的相容處理,同時也為 Promise 庫基本功能打下了保證。在 ES6 的 Promise 規範和實現出來之前為社群的 Promise 的使用提供了便利和保證。

  既然已經有了 ES6 的 Promise 的各個瀏覽器的標準實現,為什麼還要去學習 Promise A+ 規範。ES6 的 Promise 規範是在 Promise A+ 嚴格版,它規定了更加細緻的方法實現和更多的api。但是 Promise 的核心可信的非同步呼叫鏈

全在 Promise A+ 規範中規定。實現 Promise A+ 規範的 Promise,就可以在原始碼層面徹底吃透 Promise 的本質。

  其次,社群等級的開源規範能夠學習到對程式碼邊界以及異常完善的處理。在日常業務中,程式碼呼叫的輸入基本是可控的,比如:處理後端資料或是處理使用者的輸入。但是庫的程式碼在被呼叫時是未知的不可控的,書寫庫的程式碼在異常處理上需要比業務層更全面的思考和更謹慎的處理。

前置

  在進入實現前,Promise A+ 規定了幾種名詞的定義。

  • promise 物件或者函式,具備按照規範實現的then方法。
  • thenable 物件或者函式,具備then方法。
  • value
    具備合法 JavaScript 值的值(包括 undefined/thenable/promise)
  • exceptionthrow 出的 一個 value
  • reason 被 promise rejected 的一個 value

  這邊文章同樣需要一些定義,並且這些定義需要結合上面 Promise A+ 定義。

  • 決議pending 狀態的 promise
  • 對應 value 的定義
  • resolve 把一個 pending 狀態的 promise 變為 fulfilled
  • reject 把一個 pending 狀態的 promise 變為 rejected

狀態與非同步

  每一個 Promise 都應該具備三種狀態的一種 pending/fulfilled/rejected。由 pending 狀態被決議為 fulfilled/rejected狀態。決議完畢後,狀態不能再相互轉換。即 fulfilledrejected 之間是無法進行轉換的。

  首先根據 Promise 的呼叫方式以及狀態的定義先完成一個 Promise 類的基本搭建。Promise 的呼叫是傳入一個函式,改函式接收到兩個入參都為函式,使用這兩個函式可以對 Promise 進行決議。

function execute(resolve, reject) {
  if (/* something */) {
    resolve();
  }
  reject();
}

const promise = new Promise(execute);

  這裡使用 class 方式完成 Promise 實現。它具備 status 屬性表示 Promise 的狀態,value 表示 Promise 的 fulfilled 的值,reason 表示 Promise rejected 的值。

  隨後實現一個 init 方法去執行傳入的函式,並把能夠決議 Promise 的兩個函式(resolve/reject)傳入。

class _Promise {
  constructor(execute) {
    // pending | fulfilled | rejected
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this._init(execute);
  };

  _init = (execute) => {
    // 傳入函式執行時可能直接報錯,此時捕獲並 reject 掉 promise
    try {
      execute(this._resolve, this._reject);
    } catch(error) {
      this._reject(error);
    }
  };

  _resolve = () => {
    // 只有 pending 狀態能夠被轉換
    if (this.status = 'pending') {
      this.status = 'fulfilled';
    }
  };

  _reject = () => {
    if (this.status = 'pending') {
      this.status = 'rejected';
    }
  };
}

  接下來需要提 Promise 當中比較核心的一個部分 非同步。這個實現本來應該屬於下一小節 then方法,這裡提出來單獨講解。Promise A+ 原文如下:

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

  去 resolve 或者或者 reject 一個 promise 的需要等待當前執行棧其它程式碼執行完畢的時候。當然,在現在可以知道決議一個 Promise 是一個微任務,它的執行順序位於當前js程式碼執行棧的最尾端。但是 Promise A+ 規範沒有明確規定執行的順序,只需要非同步把 Promise 決議在當前程式碼執行端的最下方,因此可以使用巨集任務來實現。

  這邊實現函式delayCallback,接受一個函式,該函式會非同步在執行棧末端被呼叫。delayCallback對執行環境做了一個簡單的相容,Node環境使用process.nextTick,瀏覽器環境使用requestIdleCallback,如果不支援使用setTimeout方法。

let delayHandler;
export function delayCallback(callback) {
  if (!delayHandler) {
    if (typeof requestIdleCallback === 'function') {
      delayHandler = requestIdleCallback;
    } else if (typeof process !== 'undefined') {
      delayHandler = process.nextTick;
    } else {
      delayHandler = function (handler) {
        setTimeout(handler, 0);
      }
    }
  }
  delayHandler(callback);
}

  現在使用 delayCallback 函式對之前的程式碼做一個改造,使resolve, reject能夠按照規範非同步的去決議一個 Promise。

class _Promise {
  // ... 省略其它部分程式碼
  _resolve = value => {
    // 只有 pending 狀態能夠被轉換
    delayCallback(() => {
      if (this.status = 'pending') {
        this.status = 'fulfilled';
        this.value = value;
      }
    });
  };
  
  _reject = reason => {
    delayCallback(() => {
      if (this.status = 'pending') {
        this.status = 'rejected';
        this.reason = reason;
      }
    });
  };
}

then方法

  Promise 必須提供一個 then 方法,擁有onFulfilled/onRejected兩個函式入參,這兩個函式入參能夠去決議 Promise。如上一節非同步所提到的,這個決議必須是非同步的。且onFulfilled/onRejected不能被以 Promise 的作用域呼叫。

  onFulfilled/onRejected 只能在 Promise 被 fulfilled/rejected 之後呼叫,且只能被呼叫一次。onFulfilled/onRejected為函式之外的型別時,忽略它們。

  then方法可能被呼叫多次,每次呼叫都會返回一個 Promise,返回的 Promise 的狀態根據當前呼叫 then 方法的Promise同步。

  先根據規範做好 then 函式的框架,對入參的兩個函式做一個判斷,只要傳入的不是函式的都賦值一個預設函式。然後宣告一個名為 promise 的變數作為最後的返回值。

class _Promise {
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onRejected === 'function' ? onFulfilled : function (value) { return value };
    onRejected = typeof onRejected === 'function' ? onRejected : function (reason) { throw reason };
    let promise;
    switch(this.status) {
      case 'fulfilled':
      case 'rejected':
      case 'pending':
    }
    return promise;
  }
}

  現在來實現 Promise A+ 規範對於 Promise 不同狀態的返回值處理。先看 Promise 已經被決議的情況。

當前 Promise 已被決議

  如果當前 Promise 已經被決議,並且傳入的引數均不是函式的時候,返回一個新的 Promise,狀態和當前Promise相同,並且對應的 valuereason 也和當前 Promise 相同。而當onFulfilled/onRejected為函式的時候,會把當前 Promise 的 value/reason 作為函式的第一個入參,執行對應的函式獲得函式的返回值,並執行Resolution方法。

  Resolution將在下一節實現,現在只需要知道它接受一個 Promise 和一個值,返回一個根據值決議之後的 Promise。而如果當執行對應的onFulfilled/onRejected函式發生錯誤的時候,返回的 Promise 狀態將為 rejected,對應的 reason 即為丟擲的錯誤。

  始終不要忘記onFulfilled/onRejected被要求在執行棧的最末端非同步呼叫。

class _Promise {
  then(onFulfilled, onRejected) {
    // 不為函式時,賦值一個預設函式
    onFulfilled = typeof onRejected === 'function' ? onFulfilled : function (value) { return value };
    onRejected = typeof onRejected === 'function' ? onRejected : function (reason) { throw reason };
    let promise;
    switch(this.status) {
      case 'fulfilled':
      case 'rejected':
        promise2 = new _Promise((resolve, reject) => {
          // onFulfilled 和 onRejected 始終要求被非同步呼叫
          delayCallback(() => {
            try {
              // 根據狀態執行對應的 onFulfilled 或者 onRejected 函式
              let x = this.status === 'fulfilled' ? onFulfilled(this.value) : onRejected(this.reason);
              // 現在不需要關心 Resolve 函式,它會在下一節實現
              Resolve(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        });
        break;
      case 'pending':
    }
    return promise;
  }
}

  因為 then 函式要求返回一個新的 Promise,直接使用 new _Promise 的方式構造一個新的 Promise,使用try catch完成執行對應函式時的異常捕獲,丟擲異常直接把對應的 Promisereject

  這邊還需要提一下當onFulfilled/onRejected不為函式時,賦值兩個預設函式的思路。根據規範onFulfilled/onRejected不為函式時會被忽略,直接使用當前 Promise 的 value/reason 進行決議,這邊使用佔位函式的方式,直接返回傳入的 value 或者 throw 傳入的 reason 來實現這兩點。

當前 Promise 未被決議

  當 Promise 未被決議,還處於 pending 狀態的時候,返回的新的 Promise 需要等待當前 Promise 被決議之後,根據當前 Promise 被決議的狀態和值來決議返回的 Promise。不要忘記,then 可能會被呼叫多次,決議需要按照多次呼叫的 then 的順序。

promise
  // 其中 r 和 j 均為函式,省略具體內容
  .then(r1, j1)
  .then(r2, j2)
  .then(r3, j3)

  如上面的例子,當 promise 被決議為 fulfilled 之後會按照順序依次呼叫r1 -> r2 -> r3。同樣的,被決議為 rejected 之後會依次呼叫 j1 -> j2 -> j3。實現順序的呼叫,很明顯需要一個數組來維護通過 then 註冊的函式,並且在當前的 Promise 決議之後的方法中來呼叫。

  更改一下之前寫的 _resolve_reject 函式,在末端加上執行兩個陣列內部的函式。

class _Promise {
  // onFulfilled 註冊
  resolveHandler = [];
  // onRejected 註冊
  rejectHandler = [];

  _resolve = value => {
    // 只有 pending 狀態能夠被轉換
    delayCallback(() => {
      if (this.status = 'pending') {
        this.status = 'fulfilled';
        this.value = value;
        // 執行 resolveHandler 註冊的函式
        this.resolveHandler.forEach(handler => handler(this.value));
      }
    });
  };
  
  _reject = reason => {
    delayCallback(() => {
      if (this.status = 'pending') {
        this.status = 'rejected';
        this.reason = reason;
        // 執行 rejectHandler 註冊的函式
        this.rejectHandler.forEach(errorHandler => errorHandler(this.reason));
      }
    });
  };
}

  接下來在 then 方法中加上 case 'pending' 的邏輯,把解開 Promise 的步驟以函式的形式推入 resolveHandler/rejectHandler 陣列。

class Promise {
  then = (onFulfilled, onRejected) => {
    onFulfilled = typeof onRejected === 'function' ? onFulfilled : function (value) { return value };
    onRejected = typeof onRejected === 'function' ? onRejected : function (reason) { throw reason };

    let promise2;

    switch(this.status) {
      // ...省略 被決議後 邏輯
      case 'pending':
        promise2 = new _Promise((resolve, reject) => {
          // 推入的函式內部邏輯和上面決議 promise 的邏輯相同
          // _resolve / _reject 中已經存在 delayCallback 呼叫,此時推入的函式不需要在新增 delayCallback 邏輯
          this.resolveHandler.push(value => {
            try {
              let x = onFulfilled(value);
              Resolve(promise2, x, resolve, reject);
            } catch(error) {
              reject(error);
            }
          });
          this.rejectHandler.push(reason => {
            try {
              let x = onRejected(reason);
              Resolve(promise2, x, resolve, reject);
            } catch(error) {
              reject(error);
            }
          });
        });
    }
    return promise2;
  };
}

  寫到這裡完整的 then 方法已經完成了。Promise 中比較核心的部分非同步順序呼叫已經實現完成了。其實這一塊的邏輯非常像觀察者模式。需要註冊的函式就是觀察者中通過on去註冊的函式,在被 emit 之後去次序的呼叫它。同時也清楚了 Promise 會使用類似 try catch 的方式吞掉註冊函式呼叫時的錯誤。而在日常工作寫業務過程中經常會忘記 catch 一個 Promise 的錯誤,會導致排查非常困難。這裡可以加上一個小小的優化,當沒有註冊 rejectHandler 函式時,可以在控制檯輸出一下錯誤日誌,以便除錯的錯誤排查。

Resolution 方法

  Resolution是一個抽象方法集,它是用來解決不同 Promise 實現相容問題。它接收一個 promise 和 需要決議這個 promise 的值,返回被決議後的 promise。在前言中提到過,值(value)可以是 js 中任何型別的值,也可能是 thenable。Resolution當發現傳入的值為一個 thenable 時會不斷的嘗試去呼叫 then 方法決議它,直到為非 thenable 的值為止。現在可以拋開剛才 promise 類的實現,來單純的看 Resolution 函式的實現。

/**
 * @params {object} promise 將要被決議的 promise
 * @params { promise | thenable | any } x 用來決議 promise 的 值
 * @params { function } promiseFulfilled 決議 promise 為 fulfilled 的函式
 * @params { function } promiseRejected 決議 promise 為 rejected 的函式
*/
function Resolution(promise, x, promiseFulfilled, promiseRejected) {  }

x 為 promise

  本文實現的 _Promise 本質上也是一個 thenable,只是程式碼由自己實現,可以用可控的方式來進行解值。根據 Promise A+ 規範,當 promise 和 x 為一個物件時,此時直接丟擲一個 TypeError 來 reject 掉 promise。如果非同一個物件,如果 x 已經被決議,則根據 x 的狀態來決議掉對用的 promise。如果x沒有被決議,則等待 x 被決議之後再根據狀態決議對應的 promise。

function Resolution(promise, x, promiseFulfilled, promiseRejected) {
  if (promise === x) {
    throw new TypeError('same object');
  }
  if (x instanceof _Promise) {
    switch(x.status) {
      case 'pending':
        // x 可能是個迴圈的 promise 鏈
        x.then(function (value) {
          // value 也可能還為 promise
          Resolve(promise, value, promiseFulfilled, promiseRejected);
        }, promiseRejected);
        break;
      case 'fulfilled':
        promiseFulfilled(x.value);
        break;
      case 'rejected':
        promiseRejected(x.reason);
        break;
    }
    return;
  }
}

x 為 thenable

  前言提到過,thenable 為擁有 then 方法的物件或者函式。所以 Promise A+中規定,當x為物件或者函式時,都會嘗試取 x.then的值。如果讀取x.then發生錯誤(比如有些庫會定義了物件的 getter 函式,或者設定 then 為不可讀取),直接 reject 掉 promise。如果 x.then 不為函式,說明 x 不為一個 thenable,此時使用 x 作為 value 直接 resolve 掉 promise。

  Promise A+ 中對於 Resolution 執行步驟十分的詳細。這裡只大致說一下當 x 為 thenable 時處理大體的思路。當 x 為 thenable 時,會使用
then.call(x, promiseFulfilled, promiseRejected)方式嘗試 resolve 這個 thenable。其中有兩個注意的點:

  1. promiseFulfilled 和 promiseRejected 只能被呼叫一次,當被呼叫意味著 promise 已經被決議,會立刻停止解開 thenable 的過程。函式中使用了 標識一個訊號量 executionFlag 來實現。
  2. 呼叫then.call(x, promiseFulfilled, promiseRejected)promiseFulfilled傳入的還為一個 value,還需要再呼叫一次 Resolution 來解決。
function Resolution(promise, x, promiseFulfilled, promiseRejected) {
  // ... 省略上面實現的 x 為 promise 的邏輯
  if (
    Object.prototype.toString.call(x) === '[object Object]'
    ||
    Object.prototype.toString.call(x) === '[object Function]'
  ) {
    let then;
    let executionFlag = false;
    try {
      then = x.then;
    } catch (error) {
      promiseRejected(error);
    }
    // then 為 function
    if (Object.prototype.toString.call(then) === '[object Function]') {
      // y 也可能為 thenable
      function fulfilled (y) {
        if (executionFlag) return;
        executionFlag = true;
        Resolve(promise, y, promiseFulfilled, promiseRejected);
      }
      function rejected (reason) {
        if (executionFlag) return;
        executionFlag = true;
        promiseRejected(reason);
      }
      try {
        then.call(x, fulfilled, rejected);
      } catch (error) {
        if (executionFlag) return;
        executionFlag = true;
        promiseRejected(error);
      }
    } else {
      promiseFulfilled(x);
    }
  }
}

x 為其它合法的js值

  直接使用 x 作為 valueresolve 掉 promise。

function Resolution(promise, x, promiseFulfilled, promiseRejected) {
  // ...省略邏輯
  if (
    Object.prototype.toString.call(x) === '[object Object]'
    ||
    Object.prototype.toString.call(x) === '[object Function]'
  ) {
    // ...省略邏輯
  } else {
    promiseFulfilled(x);
  }
}

最後

  至此,一個符合 Promise A+ 規範的 _Promise 已經全部實現完畢。社群還有一個名為 promises-tests 的測試庫,可以檢驗實現的 promise 是否完整的實現了 Promise A+ 的 規範。

  本文的程式碼實現可以在這裡看到。倉庫是使用 typescript 來編寫的,並且集成了 promises-tests。具體的使用規則可以檢視倉庫的 readme

  Promise 內部的核心為如何可信賴的非同步順序呼叫函式集,正如它的名字 Promise 一樣,會給你一個承諾,你註冊的方法一定會被按照註冊的順序呼叫,以此解決了js非同步回撥不可控的問題。通過實現 Promise,也會對 Promise 內部呼叫順序有了一個清晰的認識,日後碰到什麼千奇百怪的與 Promise 相關的各種執行順序的面試題,也可以輕鬆解答出來。