解讀Promise內部實現原理

前言

早前有針對 Promise 的語法寫過博文,不過僅限入門級別,淺嘗輒止食而無味。後面一直想寫 Promise 實現,礙於理解程度有限,屢次下筆未能滿意。一拖再拖,時至今日。javascript

隨着 Promise/A+規範ECMAscript規範Promise API 制定執行落地,Javascript 異步操做的基本單位也逐漸從 callback 轉換到 promise。絕大多數JavaScript/DOM平臺新增的異步API(FetchService worker)也都是基於Promise構建的。這其中對 Promise 理解不是僅看過 API,讀過幾篇實踐就能徹底掌握的。筆者以此行文,剖析細節,伴隨讀者一塊兒成長,砥礪前行。html

本文爲前端異步編程解決方案實踐系列第二篇,主要分析 Promise 內部機制及實現原理。後續異步系列還會包括GeneratorAsync/Await相關,挖坑佔位。前端

注:本文 Promise 遵照 Promises/A+ 規範,實現參照 then/promisejava

Promise 是什麼

既然要講實現原理,難免要承前啓後交代清楚 Promise 是什麼。查閱文檔,以下:git

A promise represents the eventual result of an asynchronous operation. --Promises/A+github

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation. --ECMAscriptweb

Promises/A+ 規範中表示爲一個異步操做的最終結果,ECMAscript 規範定義爲延時或異步計算最終結果的佔位符。言簡意賅,但稍微聱牙詰屈,如何表述更淺顯易懂呢?編程

說個故事,Promise 是一個美好的承諾,承諾自己會作出正確延時或異步操做。承諾會解決callback處理異步回調可能產生的調用過早,調用過晚、調用次數過多過少、吞掉可能出現的錯誤或異常問題等。另外承諾只接受首次 resolve(..)reject(..) 決議,承諾自己狀態轉變後不會再變,承諾全部經過 then(..) 註冊的回調老是依次異步調用,承諾全部異常總會被捕獲拋出。她,是一個可信任的承諾。api

嚴謹來說,Promise 是一種封裝和組合將來值得易於複用機制,實現關注點分離、異步流程控制、異常冒泡、串行/並行控制等。數組

注:文中說起 callback 問題詳情見<<你不知道的JavaScript(中卷)>> 2.3 、3.3章節

標準解讀

Promise A+ 規範字數很少簡明扼要,但仔細翻讀,其中仍有有幾點須要引人注意。

thenable 對象

thenable 是一個定義 then(..) 方法的對象或函數。thenable 對象的存在目的是使 Promise 的實現更具備通用性,只要其暴露出一個遵循 Promise/A+ 規範的 then(..) 方法。同時也會使遵循 Promise/A+ 規範的實現能夠與那些不太規範但可用的實現能良好共存。

識別 thenable 或行爲相似 Promise 對象能夠根據其是否具備 then(..) 方法來判斷,這其實叫類型檢查也可叫鴨式辯型(duck typing)。對於 thenable 值鴨式類型檢測大體相似於:

if ( p !== null && 
     (
       typeof p === 'object' || 
       typeof p === 'function'
     ) &&
     typeof p.then === 'function'
) {
    // thenable
} else {
    // 非 thenable 
}
複製代碼
then 回調異步執行

衆所周知,Promise 實例化時傳入的函數會當即執行,then(...) 中的回調須要異步延遲調用。至於爲何要延遲調用,後文會慢慢解讀。這裏有個重要知識點,回調函數異步調用時機。

onFulfilled or onRejected must not be called until the execution context stack contains only platform code --Promise/A+

簡譯爲onFulfilledonRejected 只在執行環境堆棧僅包含平臺代碼時纔可被調用。稍有疑惑,Promise/A+ 規範又對此句加以解釋:「實踐中要確保 onFulfilledonRejected 方法異步執行,且應該在 then 方法被調用的那一輪事件循環以後的新執行棧中執行。這個事件隊列能夠採用宏任務 macro-task機制或微任務 micro-task機制來實現。」

雖然Promise A+未明確指出是以 microtask 仍是 macrotask 形式放入隊列,但 ECMAScript 規範明確指出 Promise 必須以 Promise Job 形式加入 job queues(也就是 microtask)。Job Queue 是 ES6 中新提出的概念,創建在事件循環隊列之上。job queue存在也是爲了知足一些低延遲的異步操做。

敲黑板劃重點,注意這裏 macrotask microtask 分別表示異步任務的兩種分類。在掛起任務時,JS 引擎會將全部任務按照類別分到兩個隊列中,首先在 macrotask 的隊列(也叫 task queue)中取出第一個任務,執行完畢後取出 microtask 隊列中的全部任務順序執行;以後再取 macrotask 任務,周而復始,直至兩個隊列的任務都取完。

對於microtask執行時機,whatwg HTML規範中也有闡述,詳情可點擊查閱。更多相關文章可參考附錄 event loop

再看一個示例,加深理解:

setTimeout(function () {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function () {
  console.log('promise1');
}).then(function () {
  console.log('promise2');
});
複製代碼

打印的順序?正確答案是:promise1, promise2, setTimeout

在進一步實現 Promise 對象以前,簡單模擬異步執行函數供後文Promise回調使用(也可採用 asap庫等)。

var asyncFn = function () {
  if (typeof process === 'object' && process !== null && 
      typeof(process.nextTick) === 'function'
  ) {
    return process.nextTick;
  } else if (typeof(setImmediate) === 'function') {
    return setImmediate;
  }
  return setTimeout;
}();
複製代碼
Promise 狀態

Promise 必須爲如下三種狀態之一:等待態(Pending)、執行態(Fulfilled)和拒絕態(Rejected)。一旦Promiseresolvereject,不能再遷移至其餘任何狀態(即狀態 immutable)。

爲保持代碼清晰,暫無異常處理。同時爲表述方便,約定以下:

  • fulfilled 使用 resolved 代替
  • onFulfilled 使用 onResolved 代替

Promise 構造函數

從構造函數開始,咱們一步步實現符合 Promsie A+ 規範的 Promise。大概描述下,Promise構造函數須要作什麼事情。

  1. 初始化 Promise 狀態(pending
  2. 初始化 then(..) 註冊回調處理數組(then 方法可被同一個 promise 調用屢次)
  3. 當即執行傳入的 fn 函數,傳入Promise 內部 resolvereject 函數
  4. ...
function Promise (fn) {
  // 省略非 new 實例化方式處理
  // 省略 fn 非函數異常處理

  // promise 狀態變量
  // 0 - pending
  // 1 - resolved
  // 2 - rejected
  this._state = 0;
  // promise 執行結果
  this._value = null;
 
  // then(..) 註冊回調處理數組
  this._deferreds = [];

  // 當即執行 fn 函數
  try {
    fn(value => {
      resolve(this, value);
    },reason => {
      reject(this, reason);
    })
  } catch (err) {
    // 處理執行 fn 異常
    reject(this, err);
  }
}
複製代碼

_state_value 變量很容易理解,_deferreds變量作什麼?規範描述:then 方法能夠被同一個 promise 調用屢次。爲知足屢次調用 then 註冊回調處理,內部選擇使用 _deferreds 數組存儲處理對象。具體處理對象結構,見 then 函數章節。

最後執行 fn 函數,並調用 promise 內部的私有方法 resolverejectresolvereject 內部細節隨後介紹。

then 函數

Promise A+提到規範專一於提供通用的 then 方法。then 方法能夠被同一個 promise 調用屢次,每次返回新 promise 對象 。then 方法接受兩個參數onResolvedonRejected(可選)。在 promiseresolvereject 後,全部 onResolvedonRejected 函數須按照其註冊順序依次回調,且調用次數不超過一次。

根據上述,then 函數執行流程大體爲:

  1. 實例化空 promise 對象用來返回(保持then鏈式調用)
  2. 構造 then(..) 註冊回調處理函數結構體
  3. 判斷當前 promise 狀態,pending 狀態存儲延遲處理對象 deferred ,非pending狀態執行 onResolvedonRejected 回調
  4. ...
Promise.prototype.then = function (onResolved, onRejected) {

  var res = new Promise(function () {});
  // 使用 onResolved,onRejected 實例化處理對象 Handler
  var deferred = new Handler(onResolved, onRejected, res);

  // 當前狀態爲 pendding,存儲延遲處理對象
  if (this._state === 0) {
    this._deferreds.push(deferred);
    return res;
  }

  // 當前 promise 狀態不爲 pending
  // 調用 handleResolved 執行onResolved或onRejected回調
  handleResolved(this, deferred);
  
  // 返回新 promise 對象,維持鏈式調用
  return res;
};
複製代碼

Handler 函數封裝存儲 onResolvedonRejected 函數和新生成 promise 對象。

function Handler (onResolved, onRejected, promise) {
  this.onResolved = typeof onResolved === 'function' ? onResolved : null;
  this.onRejected = typeof onRejected === 'function' ? onRejected : null;
  this.promise = promise;
}
複製代碼

鏈式調用爲何要返回新的 promise

如咱們理解,爲保證 then 函數鏈式調用,then 須要返回 promise 實例。但爲何返回新的 promise,而不直接返回 this 當前對象呢?看下面示例代碼:

var promise2 = promise1.then(function (value) {
  return Promise.reject(3)
})
複製代碼

假如 then 函數執行返回 this 調用對象自己,那麼 promise2 === promise1promise2 狀態也應該等於 promise1 同爲 resolved。而 onResolved 回調中返回狀態爲 rejected 對象。考慮到 Promise 狀態一旦 resolvedrejected就不能再遷移,因此這裏 promise2 也沒辦法轉爲回調函數返回的 rejected 狀態,產生矛盾。

handleResolved 函數功能爲根據當前 promise 狀態,異步執行 onResolvedonRejected 回調函數。因在 resolvereject 函數內部一樣須要相關功能,提取爲單獨模塊。往下翻閱查看。

resolve 函數

Promise 實例化時當即執行傳入的 fn 函數,同時傳遞內部 resolve 函數做爲參數用來改變 promise 狀態。resolve 函數簡易版邏輯大概爲:判斷並改變當前 promise 狀態,存儲 resolve(..)value 值。判斷當前是否存在 then(..) 註冊回調執行函數,若存在則依次異步執行 onResolved 回調。

但如文初所 thenable 章節描述,爲使 Promise 的實現更具備通用性,當 value 爲存在 then(..) 方法的 thenable 對象,須要作 Promise Resolution Procedure 處理,規範描述爲 [[Resolve]](promise, x)。(x 即 爲後面 value 參數)。

具體處理邏輯流程以下:

  • 若是 promisex 指向同一對象,以 TypeError 爲據因拒絕執行 promise

  • 若是 xPromise ,則使 promise 接受 x 的狀態

  • 若是 x 爲對象或函數

    1. x.then 賦值給 then
    2. 若是取 x.then 的值時拋出錯誤 e ,則以 e 爲據因拒絕 promise
    3. 若是 then 是函數,將 x 做爲函數的做用域 this 調用之。
    4. 若是 x 不爲對象或者函數,以 x 爲參數執行 promise

原文參考Promise A+規範 Promise Resolution Procedure

function resolve (promise, value) {
  // 非 pending 狀態不可變
  if (promise._state !== 0) return;
  
  // promise 和 value 指向同一對象
  // 對應 Promise A+ 規範 2.3.1
  if (value === promise) {
    return reject( promise, new TypeError('A promise cannot be resolved with itself.') );
  }
  
  // 若是 value 爲 Promise,則使 promise 接受 value 的狀態
  // 對應 Promise A+ 規範 2.3.2
  if (value && value instanceof Promise && value.then === promise.then) {
    var deferreds = promise._deferreds
    
    if (value._state === 0) {
      // value 爲 pending 狀態
      // 將 promise._deferreds 傳遞 value._deferreds
      // 偷個懶,使用 ES6 展開運算符
      // 對應 Promise A+ 規範 2.3.2.1
      value._deferreds.push(...deferreds)
    } else if (deferreds.length !== 0) {
      // value 爲 非pending 狀態
      // 使用 value 做爲當前 promise,執行 then 註冊回調處理
      // 對應 Promise A+ 規範 2.3.2.二、2.3.2.3
      for (var i = 0; i < deferreds.length; i++) {
        handleResolved(value, deferreds[i]);
      }
      // 清空 then 註冊回調處理數組
      value._deferreds = [];
    }
    return;
  }

  // value 是對象或函數
  // 對應 Promise A+ 規範 2.3.3
  if (value && (typeof value === 'object' || typeof value === 'function')) {
    try {
      // 對應 Promise A+ 規範 2.3.3.1
      var then = obj.then;
    } catch (err) {
      // 對應 Promise A+ 規範 2.3.3.2
      return reject(promise, err);
    }

    // 若是 then 是函數,將 value 做爲函數的做用域 this 調用之
    // 對應 Promise A+ 規範 2.3.3.3
    if (typeof then === 'function') {
      try {
        // 執行 then 函數
        then.call(value, function (value) {
          resolve(promise, value);
        }, function (reason) {
          reject(promise, reason);
        })
      } catch (err) {
        reject(promise, err);
      }
      return;
    }
  }
  
  // 改變 promise 內部狀態爲 `resolved`
  // 對應 Promise A+ 規範 2.3.3.四、2.3.4
  promise._state = 1;
  promise._value = value;

  // promise 存在 then 註冊回調函數
  if (promise._deferreds.length !== 0) {
    for (var i = 0; i < promise._deferreds.length; i++) {
      handleResolved(promise, promise._deferreds[i]);
    }
    // 清空 then 註冊回調處理數組
    promise._deferreds = [];
  }
}
複製代碼

resolve 函數邏輯較爲複雜,主要集中在處理 valuex)值多種可能性。若是 valuePromise 且狀態爲pending時,須使 promise 接受 value 的狀態。在 value 狀態爲 pending 時,簡單將 promisedeferreds 回調處理數組賦予 value deferreds變量。非 pending 狀態,使用 value 內部值回調 promise 註冊的 deferreds

若是 valuethenable 對象,以 value 做爲函數的做用域 this 調用之,同時回調調用內部 resolve(..)reject(..)函數。

其餘情形則以 value 爲參數執行 promise,調用 onResolvedonRejected 處理函數。

事實上,Promise A+規範 定義的 Promise Resolution Procedure 處理流程是用來處理 then(..) 註冊的 onResolvedonRejected 調用返回值 與 then 新生成 promise 之間關係。不過考慮到 fn 函數內部調用 resolve(..)產生值 與當前 promise 值仍然存在相同關係,邏輯一致,寫進相同模塊。

reject 函數

Promise 內部私有方法 reject 相較於 resolve 邏輯簡單不少。以下所示:

function reject (promise, reason) {
  // 非 pending 狀態不可變
  if (promise._state !== 0) return;

  // 改變 promise 內部狀態爲 `rejected`
  promise._state = 2;
  promise._value = reason;

  // 判斷是否存在 then(..) 註冊回調處理
  if (promise._deferreds.length !== 0) {
    // 異步執行回調函數
    for (var i = 0; i < promise._deferreds.length; i++) {
      handleResolved(promise, promise._deferreds[i]);
    }
    promise._deferreds = [];
  }
}
複製代碼

handleResolved 函數

瞭解完 Promise 構造函數、then 函數、以及內部 resolvereject 函數實現,你會發現其中全部的回調執行咱們都統一調用 handleResolved函數,那 handleResolved 到底作了哪些事情,實現又有什麼注意點?

handleResolved 函數具體會根據 promise 當前狀態判斷調用 onResolvedonRejected,處理 then(..) 註冊回調爲空情形,以及維護鏈式 then(..) 函數後續調用。具體實現以下:

function handleResolved (promise, deferred) {
  // 異步執行註冊回調
  asyncFn(function () {
    var cb = promise._state === 1 ? 
            deferred.onResolved : deferred.onRejected;

    // 傳遞註冊回調函數爲空狀況
    if (cb === null) {
      if (promise._state === 1) {
        resolve(deferred.promise, promise._value);
      } else {
        reject(deferred.promise, promise._value);
      }
      return;
    }

    // 執行註冊回調操做
    try {
      var res = cb(promise._value);
    } catch (err) {
      reject(deferred.promise, err);
    }

    // 處理鏈式 then(..) 註冊處理函數調用
    resolve(deferred.promise, res);
  });
}
複製代碼

具體處理註冊回調函數 cb 爲空情形,以下面示例。判斷當前回調 cb 爲空時,使用 deferred.promise 做爲當前 promise 結合 value 調用後續處理函數繼續日後執行,實現值穿透空處理函數日後傳遞。

Promise.resolve(233)
  .then()
  .then(function (value) {
    console.log(value)
  })
複製代碼

關於 then 鏈式調用,簡單再說下。實現 then 函數的鏈式調用,只須要在 Promise.prototype.then(..) 處理函數中返回新的 promise 實例便可。但除此以外,還須要依次調用 then 註冊的回調處理函數。如 handleResolved 函數最後一句 resolve(deferred.promise, res) 所示。

then 註冊回調函數爲何異步執行

這裏回答開篇所提到的一個問題,then 註冊的 onResolvedonRejected 函數爲何要採用異步執行?再來看一段實例代碼。

var a = 1;

promise1.then(function (value) {
  a = 2;
})

console.log(a)
複製代碼

promise1 內部執行同步或異步操做未知。假如未規定 then 註冊回調爲異步執行,則這裏打印 a 可能存在兩種值。promise1 內部同步操時 a === 2,相反執行異步操做時 a === 1。爲屏蔽依賴外部的不肯定性,規範指定 onFulfilledonRejected 方法異步執行。

promise 內部錯誤或異常

若是 promiserejected,則會調用拒絕回調並傳入拒由。好比在 Promise 的建立過程當中(fn執行時)出現異常,那這個異常會被捕捉並調用 onRejected

但還存在一處細節,若是 Promise 完成後調用 onResolved 查看結果時出現異常錯誤會怎麼樣呢?注意此時 onRejected 不會被觸發執行,由於 onResolved 內部異常並不會改變當前 promise 狀態(仍爲resolved),而是改變 then 中返回新的 promise 狀態爲 rejected。異常未丟失但也未調用錯誤處理函數。

如何處理?Ecmascript規範有定義Promise.prototype.catch方法,假如你對 onResolved 處理過程沒有信心或存在異常 case 狀況,最好仍是在 then 函數後調用 catch 方法作異常捕獲兜底處理。

Promise 相關方法實現

查閱 Promise 相關文檔或書籍,你還會發現 Promise 相關有用的API:Promise.racePromise.allPromise.resolvePromise.reject。這裏對 Promise.race 方法實現作個展現,剩餘可自行參考實現。

Promise.race = function (values) {
  return new Promise(function (resolve, reject) {
    values.forEach(function(value) {
      Promise.resolve(value).then(resolve, reject);
    });
  });
};
複製代碼

結語

寫到這裏,核心的 Promise 實現也逐漸完成,Promise 內部細節也在文中或代碼中一一描述。限於筆者自己能力有限,對於 promise 內部實現暫未達到庖丁解牛程度,有些地方一筆帶過,可能讀者心生疑惑。針對不解的地方,建議多讀兩遍或參考書籍理解。

若是讀完拙文能多少有點收穫,也算達到筆者初衷,你們一塊兒成長。最後筆者也非完人,文中難免語句不順或詞不達意,望理解。若是對於本文有任何疑問或錯誤,歡迎斧正,在此先行謝過。

附錄

參考文檔

  1. ECMA262 Promise

  2. Promises/A+ Specification

  3. [譯] Promises/A+ 規範

  4. then/promise

  5. 寫一個符合 Promises/A+ 規範並可配合 ES7 async/await 使用的 Promise

  6. 剖析Promise內部結構,一步一步實現一個完整的、能經過全部Test case的Promise類

  7. 剖析 Promise之基礎篇

參考書籍

  1. 《你不知道的JavaScript(中卷)》

  2. 《深刻理解ES6》

  3. 《JavaScript框架設計(第2版)》

  4. 《ES6標準入門(第3版)》

event loop

  1. Tasks, microtasks, queues and schedules

  2. [譯]Tasks, microtasks, queues and schedules

  3. Difference between microtask and macrotask within an event loop context

  4. 從event loop規範探究javaScript異步及瀏覽器更新渲染時機

  5. 深刻探究 eventloop 與瀏覽器渲染的時序問題

打個廣告,歡迎關注筆者公衆號

相關文章
相關標籤/搜索