『你寫的Promise, 是完美的嗎?』

斑駁光影發表於2022-07-15

歡迎來這裡 前端雜談, 聊聊前端

程式碼在github

《手寫 Promise》是一個經典的問題,基本上大家上手都可以按照自己的理解,寫出來一個 promise, 有一天個朋友問我,"手寫 Promise 要寫到什麼程度才是合格的 ?", 這也引起了我的興趣和思考, "怎麼樣的 Promise ,才是完美的呢 ? "

完美的 Promise

第一個問題就是怎麼樣才算是一個完美的 Promise 呢, 其實這個問題也不難,實現一個和原生 Promise "相同"的 Promsie,不就是完美的了, 那麼第二個問題也就來了,原生的 Promise 是按照什麼標準來實現的呢, 查閱了資料之後知道是按照 [Promises/A+] (https://promisesaplus.com/)標準來實現的, 具體的實現在 ECMA - sec-promise-objects 上有記載, 現在標準有了,我們就可以來實現一個"完美的 Promise"了

Promises/A+

接下來我們來看看Promises/A+標準說了啥, 主要是兩部分,一個是名詞定義,一個是標準描述,其中標準描述由三個部分組成, 接下來我們簡單介紹下:

Terminology

這部分是名詞定義,主要是描述了各個名詞在標準中的定義

  • promise: 是具有then行為符合規範的方法的objectfunction, 這裡需要注意的是不是functionthen,是function中有then 方法
  • thenable: 是定義then方法的object函式,這個和上面promise的區別在於then是一個函式,不一定需要符合規範行為
  • value: 是任何合法的 javascript 值,包括undefinedthenablepromise ,這裡的value包含了thenablepromise,結合下面的規範,會發現是一個可巢狀的關係
  • exception: 是一個通過throw 關鍵詞丟擲來的值
  • reason: 表示一個promise狀態是rejected 的原因

Requirements

這部分是標準的定義,分為以下三個部分

Promise States

一個promise必須是以下三種狀態之一

  • pending

    • 可以轉變成 fulfilled 或者 rejected 狀態
  • fulfilled

    • 需要存在一個value
  • rejected

    • 需要存在一個reason

當狀態是fulfilled 或者 rejected時,狀態不可以再變化成其他狀態,而valuereason 也不可以再變化

The then Method

這部分定義了 promisethen 方法的行為,then 方法是用來訪問promise狀態變成fulfilled 或者 rejectedvalue 或者reason 的, then 有兩個引數,如下:

promise.then(onFulfilled,onRejected)
  • onFulfilled / onRejected

    • 都是可選引數,如果這兩個引數不是函式型別,那麼忽略
    • promise狀態變成fulfilled/rejected 之後被呼叫,會帶上value/reason 作為函式的引數
    • 只會被呼叫一次
    • 需要在巨集任務或者微任務 事件迴圈中完成。 注: 這裡對於執行時機的描述比較有趣,可以看看文件 2.2.4
    • 兩個函式需要被繫結在global this上執行
  • 同一個 Promise可以被多次 then 呼叫, then 中的 onFulfilledonRejected 必須按照then的呼叫順序呼叫
  • then 函式呼叫之後需要返回一個promise , 這也是promise可以鏈式呼叫then的基礎

    promise2 = promise1.then(onFulfilled,onRejected)
    • 如果onFulfilled或者onRejected函式返回了值x, 則執行 Promise Resolution Procedure
    • 如果onFulfilled或者onRejected 丟擲錯誤e, 則 promise2 的狀態是rejected,並且reasone
    • 如果onFulfilled或者onRejected不是一個函式,而且promise1的狀態已經確定fulfilled/rejected, 則 promise2

The Promise Resolution Procedure

其實大體的標準部分在Promise StatesThe then Method已經描述完了,這部分主要規定了一個抽象的操作promise resolution procedure, 用來描述當thenonFulfilled或者onRejected 返回值x時,需要怎麼樣去進行操作,把表示式記為[[Resolve]](promise,x), 這部分也是整個 Promise 實現最複雜的部分,我們一起看看他規定了什麼

[[Resolve]](promise,x)
  • promisex 是同一個物件時,promiserejected,reasonTypeError

    const promise = Promise.resolve().then(()=>promise); // TypeError
    
  • 如果 x 是一個Promise時,則promise的狀態要與x 同步
  • 如果x是一個object或者一個function , 這部分是最複雜的

    • 首先要把x.then儲存在一箇中間變數then, 為什麼要這麼做可以看文件 3.5,然後根據不同條件進行處理
    • 如果獲取x.then 的時候就丟擲錯誤e,則promise 狀態變成rejected,reasone
    • 如果then是一個函式,那麼這就是我們定義裡面的thenable, 這時候繫結 x為 this並呼叫then,傳入 promiseresolvePromiserejectPromise作為兩個引數

      then.call(x, resolvePromise, rejectPromise)

      接下來判斷呼叫的結果

      • 如果resolvePromise 被呼叫,valuey, 則呼叫[[Resolve]](promise,y)
      • 如果rejectPromise 被呼叫, reasone, 則 promise 狀態變成rejected, reasone
      • 如果resolvePromiserejectPromise都被呼叫,則以第一個呼叫會準,後續的呼叫都被忽略
      • 如果呼叫過程中丟擲了錯誤e

        • 如果丟擲之前resolvePromise 或者rejectPromise已經被呼叫了,那麼就忽略錯誤
        • 後者的話,則promise狀態變成rejected,reasone
    • 如果then 不是一個函式,那麼promise狀態變成fulfilled,valuex
  • 如果 x 不是一個object 或者function, 則promise狀態變成fulfilled,valuex

這裡面最複雜的就是在 resolvePromise 被呼叫,valuey 這部分,實現的是thenable 的遞迴函式

上面就是如何實現一個"完美"的 Promise 的規範了,總的來說比較複雜的是在The Promise Resolution Procedure 和對於錯誤和呼叫邊界的情況,下面我們將開始動手,實現一個"完美"的Promise

如何測試你的 Promise

前面介紹了 Promise/A+規範, 那麼如何測試你的實現是完全實現了規範的呢, 這裡Promise/A+ 提供了 [promises-tests
](https://github.com/promises-a...), 裡面目前包含872個測試用例,用於測試 Promise 是否標準

正文開始

首先說明下這邊是按照已完成的程式碼對實現 promise 進行介紹程式碼在這裡, 這裡使用的是最終版本,裡面註釋大致標明瞭實現的規則編號,其實整體來說經過了很多修改,如果要看整個便攜過程,可以commit history, 關注promise_2.jspromise.js 兩個檔案

編寫的關鍵點

整體的實現思路主要就是上面的規範了,當然我們也不是說逐條進行實現,而是對規範進行分類,統一去實現:

promise的狀態定義及轉變規則和基礎執行

const Promise_State = {
  PENDING: "pending",
  FULFILLED: "fulfilled",
  REJECTED: "rejected",
};

class MyPromise {
  constructor(executerFn) {
    this.state = Promise_State.PENDING;
    this.thenSet = [];
    try {
      executerFn(this._resolveFn.bind(this), this._rejectedFn.bind(this));
    } catch (e) {
      this._rejectedFn.call(this, e);
    }
  }
}

在建構函式中初始化狀態為pending,並且執行傳入建構函式的executerFn,傳入resovlePromiserejectePromise兩個引數

然後我們接下去就要實現 resolvePromise,rejectPromise 這兩個函式

  _resolveFn(result) {
    // 2.1.2
    if (this._checkStateCanChange()) {
      this.state = Promise_State.FULFILLED;
      this.result = result;
      this._tryRunThen();
    }
  }

  _rejectedFn(rejectedReason) {
    //2.1.3
    if (this._checkStateCanChange()) {
      this.state = Promise_State.REJECTED;
      this.rejectedReason = rejectedReason;
      this._tryRunThen();
    }
  }

  _checkStateCanChange() {
    //2.1.1
    return this.state === Promise_State.PENDING;
  }

這裡主要是通過_checkStateCanChange 判斷是否可執行的狀態,然後進行狀態變更,valuereason的賦值,然後嘗試執行then方法註冊的函式

這時候我們的promise 已經可以這麼呼叫了

const p = new MyPromise((resolve,reject)=>{
   resolve('do resolve');
   // reject('do reject');
});

then的實現

接下來我們實現then 函式,首先有個簡單的問題: 『then方法是什麼時候執行的?』,有人會回答,是在 promise 狀態變成resolve或者rejected 的之後執行的,這個乍一看好像沒毛病,但是其實是有毛病的,正確的說法應該是

『then方法是立即執行的,then方法傳入的onFulfilledonRejected 引數會在 promise 狀態變成resolve 或者rejected後執行

我們先上程式碼


  then(onFulfilled, onRejected) {
    const nextThen = [];
    const nextPromise = new MyPromise((resolve, reject) => {
      nextThen[1] = resolve;
      nextThen[2] = reject;
    });
    nextThen[0] = nextPromise;

    //2.2.6
    this.thenSet.push([onFulfilled, onRejected, nextThen]);
    this._runMicroTask(() => this._tryRunThen());
    return nextThen[0];
  }

程式碼看起來也挺簡單的,主要邏輯就是構造一個新的 promise,然後把 onFulfilledonRejected還有新構造的 promise 的resolvereject 儲存到thenSet集合中,然後返回這個新構建的promise, 這時候我們的程式碼已經可以這樣子呼叫


const p = new MyPromise((resolve,reject)=>{
   resolve('do resolve');
   // reject('do reject');
});

p.then((value)=>{
  console.log(`resolve p1 ${value}`);
},(reason)=>{
  console.log(`reject p1 ${reason}`);
}).then((value)=>console.log(`resolve pp1 ${value}`));

p.then((value)=>{
  console.log(`resolve p2 ${value}`);
},(reason)=>{
  console.log(`reject p2 ${reason}`);
});

onFulfilled和onRejected的執行及執行時機

onFulFilledonRejected 會在 promise 狀態變成fulfilled或者rejected之後被呼叫,結合then方法被呼叫的時機,判斷時候狀態可以呼叫需要在兩個地方做

  • resolvePromiseresolvePromise 被呼叫的時候(判斷是否有呼叫了then註冊了onFulfilledonRejected)
  • then 函式被呼叫的時候(判斷是否 promise狀態已經變成了fulfilledrejected)

    這兩個時機會呼叫以下函式

    
    _tryRunThen() {
     if (this.state !== Promise_State.PENDING) {
       //2.2.6
       while (this.thenSet.length) {
         const thenFn = this.thenSet.shift();
         if (this.state === Promise_State.FULFILLED) {
           this._runThenFulfilled(thenFn);
         } else if (this.state === Promise_State.REJECTED) {
           this._runThenRejected(thenFn);
         }
       }
     }
    }
    

    這裡會判斷時候需要呼叫then註冊的函式,然後根據 promise 的狀態將 thenSet 中的函式進行對應的呼叫


  _runThenFulfilled(thenFn) {
    const onFulfilledFn = thenFn[0];
    const [resolve, reject] = this._runBothOneTimeFunction(
      thenFn[2][1],
      thenFn[2][2]
    );
    if (!onFulfilledFn || typeOf(onFulfilledFn) !== "Function") {
      // 2.2.73
      resolve(this.result);
    } else {
      this._runThenWrap(
        onFulfilledFn,
        this.result,
        thenFn[2][0],
        resolve,
        reject
      );
    }
  }

_runThenFulfilled_runThenRejected 相似,這裡就通過一個進行講解,
首先我們判斷onFulfilled或者onRejected 的合法性

  • 如果不合法則不執行,直接將promise 的valuereason透傳給之前返回給then 的那個 promise,這個時候相當於then的 promise 的狀態和原來的 promise 的狀態相同
  • 如果合法,則執行onFulfilled 或者 onRejected
  _runThenWrap(onFn, fnVal, prevPromise, resolve, reject) {
     this._runMicroTask(() => {
        try {
          const thenResult = onFn(fnVal);
          if (thenResult instanceof MyPromise) {
            if (prevPromise === thenResult) {
              //2.3.1
              reject(new TypeError());
            } else {
              //2.3.2
              thenResult.then(resolve, reject);
            }
          } else {
            // ... thenable handler code
            // 2.3.3.4
            // 2.3.4
            resolve(thenResult);
          }
        } catch (e) {
          reject(e);
        }
     });
  }

這裡先擷取一小段_runThenWrap,主要是說明onFulfilledonRejected的執行,這部分在規範中有這樣子的一個描述

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

簡單來說就是onFulfilledonRejected要在執行上下文裡面沒有除了platform code 之後才能執行,這段聽起來有點拗口,其實說人話就是我們經常說的要在微任務巨集任務
所以我們這裡包裝了_runMicroTask方法,用於封裝這部分執行的邏輯

   _runMicroTask(fn) {
    // 2.2.4
    queueMicrotask(fn);
  }

這裡使用queueMicrotask作為微任務的實現, 當然這個有相容性問題,具體可以看caniuse

實現的方法還有很多,比如setTimeoutsetImmediateMutationObserverprocess.nextTick

然後將valuereason作為引數執行onFulfilledonRejected,然後獲取返回值thenResult,接下來就會有幾個判斷的分支

  • 如果thenResult是一個 promise

    • 判斷是否和then返回的 promise 是相同的,如果是丟擲TypeError
    • 傳遞then返回的 promise 的resolvereject,作為thenResult.thenonFulFilledonRejected函式
  • 如果thenResult不是一個 promise

    • 判斷是否是thenable,這部分我們在下面進行講解
    • 如果以上判斷都不是,那麼將thenResult 作為引數,呼叫resolvePromise

thenable的處理

thenable應該說是實現裡面最複雜的一個部分了,首先,我們要根據定義判斷上部分的thenResult是否是thenable


   if (
      typeOf(thenResult) === "Object" ||
      typeOf(thenResult) === "Function"
    ) {
      //2.3.3.1
      const thenFunction = thenResult.then;
      if (typeOf(thenFunction) === "Function") {
        // is thenable
      }
    }

可以看到 需要判斷是否是一個Object或者Function,然後再判斷thenResult.then 是不是個 Function,那麼有人會問,能不能寫成這樣子

   if (
      (typeOf(thenResult) === "Object" ||
      typeOf(thenResult) === "Function") && (typeOf(thenResult.then) === 'Function')
    ) {
        // is thenable
    }

剛開始我也是這麼寫的,然後發現測試用例跑不過,最後去看了規範,有這麼一段3.5

簡單來說就是為了保證測試和呼叫的一致性,先把thenResult.then進行儲存再判斷和執行是有必要的,多次訪問屬性可能會返回不同的值

接下去就是thenable的處理邏輯了
簡單來說thenable 的處理邏輯有兩種情況

  • 在 promise 的 then 或者 resolve 中處理 thenable 的情況
  • thenablethen回撥中處理value 還是thenable的情況

這裡用在 promise 的thenthenable呼叫進行講述:



    _thenableResolve(result, resolve, reject) {
      try {
        if (result instanceof MyPromise) {
          // 2.3.2
          result.then(resolve, reject);
          return true;
        }

        if (typeOf(result) === "Object" || typeOf(result) === "Function") {
          const thenFn = result.then;
          if (typeOf(thenFn) === "Function") {
            // 2.3.3.3
            thenFn(resolve, reject);
            return true;
          }
        }
      } catch (e) {
        //2.3.3.3.4
        reject(e);
        return true;
      }
    }

    const [resolvePromise, rejectPromise] =
          this._runBothOneTimeFunction(
            (result) => {
              if (!this._thenableResolve(result, resolve, reject)) {
                resolve(result);
              }
            },
            (errorReason) => {
              reject(errorReason);
            }
          );

    try {
      thenFunction.call(thenResult, resolvePromise, rejectPromise);
    } catch (e) {
      //2.3.3.2
      rejectPromise(e);
    }

這裡我們構造了resolvePromiserejectPromise,然後呼叫 thenFunction, 在函式邏輯中處理完成之後將會呼叫resolvePromise或者rejectPromise, 這時候如果result是一個 thenable,那麼就會繼續傳遞下去,直到不是thenable,呼叫resolve或者reject

我們要注意的是 promise 的then方法和thenablethen方法是有不同的地方的

  • promise 的then有兩個引數,一個是fulfilled,一個是rejected,在前面的 promise狀態改變之後會回撥對應的函式
  • thenablethen 也有兩個引數,這兩個引數是提供給thenable 呼叫完成進行回撥的resolvereject 方法,如果 thenable 的回撥值還是一個thenable,那麼會按照這個邏輯呼叫下去,直到是一個非thenable,就會呼叫離thenable往上回溯最近的一個 promies 的resolve 或者reject

    到這裡,我們的promise 已經可以支援thenable的執行

    
    new MyPromise((resolve)=>{
     resolve({
       then:(onFulfilled,onRejected)=>{
         console.log('do something');
         onFulfilled('hello');
       }
     })
    }).then((result)=>{
    
     return {
       then:(onFulfilled,onRejected)=>{
         onRejected('world');
       }
     }
    });
    
    

promise和then及thenable中對於錯誤的處理

錯誤處理指的是在執行過程中出現的錯誤要進行捕獲處理,基本上使用 try/catch 在捕獲到錯誤之後呼叫 reject 回撥,這部分比較簡單,可以直接看程式碼

resolve和reject函式的呼叫次數問題

一個 promise 中的resolvereject呼叫可以說是互斥而且唯一的,就是這兩個函式只能有一個被呼叫,而且呼叫一次,這個說起來比較簡單,但是和錯誤場景在一起的時候,就會有一定的複雜性
本來可能是這樣子的

if(something true){
  resolve();
}else {
  reject();
}

加上錯誤場景之後

try{
  if(something true){
    resolve();
    throw "some error";
  }else {
    reject();
  }
}catch(e){
  reject(e);
}

這時候判斷就會無效了, 因此我們按照通過一個工具類來包裝出兩個互斥的函式,來達到目的

  _runBothOneTimeFunction(resolveFn, rejectFn) {
    let isRun = false;

    function getMutuallyExclusiveFn(fn) {
      return function (val) {
        if (!isRun) {
          isRun = true;
          fn(val);
        }
      };
    }
    return [
      getMutuallyExclusiveFn(resolveFn),
      getMutuallyExclusiveFn(rejectFn),
    ];
  }

至此,我們一個完全符合Promise/A+ 標準的 Promise,就完成了, 完整程式碼在這裡

等等,是不是少了些什麼

有人看到這裡會說,這就完了嗎?
我經常使用的catchfinally,還有靜態方法Promise.resolvePromise.rejectPromise.all/race/any/allSettled方法呢?

其實從標準來說,Promise/A+的標準就是前面講述的部分,只定義了then方法,而我們日常使用的其他方法,其實也都是在then 方法上面去派生的,比如catch 方法

 MyPromise.prototype.catch = function (catchFn) {
  return this.then(null, catchFn);
};

具體的方法其實也實現了,具體可以看promise_api

最後

最後是想分享下這次這個 promise 編寫的過程,從上面的講述看似很順利,但是其實在編寫的時候,我基本上是簡單了過了以下標準,然後按照自己的理解,結合promises-tests單元測試用例來編寫的,這種開發模式其實就是TDD(測試驅動開發 (Test-driven development)),這種開發模式會大大減輕發人員程式設計時候對於邊界場景沒有覆蓋的心智負擔,但是反過來,對於測試用例的便攜質量要求就很高了
總體來說這次便攜 promise 是一個比較有趣的過程,上面如果有什麼問題的,歡迎留言多多交流

相關文章