Promise-Polyfill原始碼解析(1)

Codeeeee發表於2018-09-24

平時在專案中經常使用到Promise,很好奇其內部的實現,發現promise-polyfill的實現非常符合Promise標準,特地花幾天細讀了下。

我們平時都是以new Promise(params)的形式使用Promise的,說明Promise是一個建構函式,那我們就從建構函式為入口來分析Promise-polyfill原始碼。如下:

/**
 * @constructor
 * @param {Function} fn
 */
function Promise(fn) {
  if (!(this instanceof Promise))
    throw new TypeError('Promises must be constructed via new');
  if (typeof fn !== 'function') throw new TypeError('not a function');
  /** @type {!number} */
  this._state = 0;
  /** @type {!boolean} */
  this._handled = false;
  /** @type {Promise|undefined} */
  this._value = undefined;
  /** @type {!Array<!Function>} */
  this._deferreds = [];

  doResolve(fn, this);
}
複製程式碼

第一個if語句說明Promise必須以建構函式形式被呼叫,第二個if語句則說明Promise的唯一引數fn必須是函式型別。接下來是四個物件屬性的定義,我們逐一來看:

 /** @type {!number} */
  this._state = 0;
複製程式碼

_state屬性定義了Promise的狀態,我們都知道Promise有pending、fulfilled、rejected三種狀態,在原始碼裡,三種狀態分別對應_state值為0、1、2。此外,原始碼中還有_state值為3,第四種內部狀態,這個我們後面遇到再講。

 /** @type {!boolean} */
  this._handled = false;
複製程式碼

_handled屬性的型別為Boolean,初始值為false,其代表Promise是否被處理。

 /** @type {Promise|undefined} */
  this._value = undefined;
複製程式碼

_value屬性的型別為Promise或undefined,初始值為undefined,其代表。

 /** @type {!Array<!Function>} */
  this._deferreds = [];
複製程式碼

_deferreds屬性的型別為Array,初始值為空陣列,其作用我們後面遇到再講,現在只要注意其陣列中存放的值為Function。 最後是一個函式呼叫:

 doResolve(fn, this);
複製程式碼

將Promise的引數fn與代表當前物件的this作為引數,呼叫了deResolve函式。至此,我們可以發現整個建構函式只是在做一些必要的檢查和屬性定義,並沒有做什麼處理,那關鍵點應該就在最後的函式呼叫。我們來看看deResolve函式都做了些什麼:

 function doResolve(fn, self) {
  var done = false;
  try {
    fn(
      function(value) {
        if (done) return;
        done = true;
        resolve(self, value);
      },
      function(reason) {
        if (done) return;
        done = true;
        reject(self, reason);
      }
    );
  } catch (ex) {
    if (done) return;
    done = true;
    reject(self, ex);
  }
}
複製程式碼

總體上看,首先定義了一個變數done,初始值為false,接下來是一個try..catch語句,我們先來分析try部分:

 try {
    fn(
      function(value) {
        if (done) return;
        done = true;
        resolve(self, value);
      },
      function(reason) {
        if (done) return;
        done = true;
        reject(self, reason);
      }
    );
  } 
複製程式碼

上面講過fn就是建構函式的引數,也就是我們new Promise時傳入的回撥函式:

new Promise(function(resolve, reject) {
  // do something
});
複製程式碼

我們用resolve, reject替換fn中的兩個引數,結果變成:

fn(resolve, reject);
複製程式碼

也就是說try部分總共就做了一件事,就是講我們傳入的回撥函式執行了,並傳入了兩個回撥函式作為引數。這裡特別注意一點,到目前為止,並沒有涉及到非同步之類的,所以我們可以知道Promise建構函式內的程式碼是同步執行的! 那麼傳入的兩個回撥函式是什麼時候被執行的呢?其實就是在我們呼叫resolve(value)或reject(reason)的時候:

new Promise(function(resolve, reject) {
  // do something
  // resolve(value);
  reject(reason);
});
複製程式碼

我們再看兩個回撥函式的內部邏輯,兩者唯一的差別就是最後呼叫的函式不同,我們先看相同的部分:

 if (done) return;
 done = true;
複製程式碼

done變數為true則直接退出函式,否則將done置為true,再執行下面程式碼。所以我們知道,done變數的作用就是為了防止resolve()和reject()被同時呼叫。因為Promise標準規定了,其狀態只能從pending->fulfilled或pending->rejected。 再看不同部分:

 resolve(self, value);
複製程式碼
 reject(self, reason);
複製程式碼

這兩個函式的引數是當前物件和我們傳入的值,也就是我們所說的完成的值和拒絕的原因,由此我們可以預測,呼叫這兩個函式會將Promsie的狀態變為fulfilled或rejected。 最後看catch部分:

 catch (ex) {
    if (done) return;
    done = true;
    reject(self, ex);
  }
複製程式碼

其邏輯完全與try部分的第二個回撥函式一樣,其實就是說,呼叫Promsie建構函式如果丟擲異常,則Promise就會變為rejected狀態。 接下來分析resolve與reject函式:

function resolve(self, newValue) {
  try {
    if (newValue === self)
      throw new TypeError('A promise cannot be resolved with itself.');
    if (
      newValue &&
      (typeof newValue === 'object' || typeof newValue === 'function')
    ) {
      var then = newValue.then;
      if (newValue instanceof Promise) {
        self._state = 3;
        self._value = newValue;
        finale(self);
        return;
      } else if (typeof then === 'function') {
        doResolve(bind(then, newValue), self);
        return;
      }
    }
    self._state = 1;
    self._value = newValue;
    finale(self);
  } catch (e) {
    reject(self, e);
  }
}
複製程式碼

整個程式碼被try...catch包裹,先看只有一行程式碼的catch部分:

 reject(self, e);
複製程式碼

整個resolve函式丟擲異常,都會呼叫reject函式,所以我們也明白了,resolve後的狀態不一定就是fulfilled,也可能是rejected,但reject後的狀態一定是rejected。 再看try部分,我們先跳過前面二個條件判斷,直接看最後的部分:

 self._state = 1;
 self._value = newValue;
 finale(self);
複製程式碼

_state屬性賦值為1,前面講過,1代表狀態為fulfilled。_value儲存了完成的值,最後將當前物件作為引數呼叫了finale函式。finale主要為then方法做準備的,與Promise建構函式關係不大,我們講then方法時再分析。 然後是第一個條件檢測:

  if (newValue === self)
      throw new TypeError('A promise cannot be resolved with itself.');
複製程式碼

newValue是我們傳入的完成的值,self是當前的Promise物件,也就是說,完成的值不能是當前物件本身。就是下面這種情況:

const promise = new Promise(function (resolve, reject) {
  setTimeout(function () {
    resolve(promise);
  }, 0);
});
複製程式碼

用非同步的原因是保證resolve(promise)時,promise已經被賦值。 第二個條件主要是處理特殊型別的完成值:

if ( newValue && (typeof newValue === 'object' || typeof newValue === 'function') ) {
  var then = newValue.then;
  //...
} 
複製程式碼

如果newValue是物件或函式型別,就將其then屬性儲存在then變數中。 往下講之前,我們需要知道一個概念:thenable型別,擁有then方法的物件或函式。這個定義其實是借鑑了鴨子型別:如果它看起來像一隻鴨子,並且叫起來相一致鴨子,那麼它一定是一隻鴨子。為什麼要提這個呢?因為我們需要判斷一個值是否是純粹的Promise物件,具體由來就不講了,推薦大家去看《你不知道的JavaScript 中卷》。 知道thenable型別,我們就清楚下面的程式碼是做什麼的了:

 if (newValue instanceof Promise) {
  self._state = 3;
  self._value = newValue;
  finale(self);
  return;
 }
複製程式碼

為什麼這個判斷要先判斷?因為Promise也有then方法,所以要先判斷值是不是純粹的Promise。以_state=3標記。再判斷是否是thenable型別:

 else if (typeof then === 'function') {
  doResolve(bind(then, newValue), self);
  return;
}
複製程式碼

其中bind函式為Function.prototype.bind的polyfill:

function bind(fn, thisArg) {
  return function() {
    fn.apply(thisArg, arguments);
  };
}
複製程式碼

即將以newValue為this的函式和當前物件作為引數再次呼叫doResolve函式,這麼做的原因,是如果Promise的完成的值是Promise或thenable型別,那麼最終狀態取決於Promise或thenable的狀態。如下:

image.png

總結:Promise建構函式除了執行了我們傳入的回撥函式,還會儲存Promise的狀態以及相應的完成的值或拒絕的原因。

最後,祝大家中秋快樂!

相關文章