Promise-Polyfill原始碼解析(2)

Codeeeee發表於2018-10-03

在上篇文章Promise-Polyfill原始碼解析(1)詳細分析了Promise建構函式部分的原始碼,本篇我們繼續分析剩下的原始碼。 本篇我們重點分析then方法,讓我們回憶下then方法的使用方式:首先這個方法屬於每個Promise物件,這說明then方法應該定義在Promise的原型鏈上;然後這個方法接收兩個回撥函式,如果Promsie的狀態為已完成,則執行第一個回撥,狀態為被拒絕,則執行第二個回撥,這個說明then方法會等待Promise狀態改變才會去執行回撥;最後then方法可以鏈式呼叫,如下:

Promise.resolve().then(function() {
  // ...
}, function() {
  // ...
}).then(function() {
  // ...
}, function() {
  // ...
});
複製程式碼

瞭解了以上,我們來看then方法的原始碼:

Promise.prototype.then = function(onFulfilled, onRejected) {
  // @ts-ignore
  var prom = new this.constructor(noop);

  handle(this, new Handler(onFulfilled, onRejected, prom));
  return prom;
};
複製程式碼

正如我們所猜想的,then方法定義在Promise的建構函式上,每個Promise物件可以共享該方法。其接收兩個引數onFulfilled、onRejected。具體實現也非常簡潔,只有三行程式碼,先來看第一行:

var prom = new this.constructor(noop);
複製程式碼

這句程式碼用new操作符例項化了一個物件,並儲存在prom變數中。new操作符的右邊一定是個建構函式,this指向當前Promise物件,其constructor屬性指向建構函式,所以this.constructor指向Promise建構函式。我們知道,Promise建構函式的引數為一個函式,這裡傳入了noop,noop是什麼?我們找到其定義:

function noop() {}
複製程式碼

我們發現noop只是個空函式。再來看最後一行程式碼:

return prom;
複製程式碼

返回了prom物件,也就是說,then方法最後返回了一個Promise物件,這也就是then方法可以鏈式呼叫的原因所在! 有個疑問,為什麼不直接返回this,而是返回新建立的Promise物件呢?其實是因為Promise的狀態改變時單向的,且只能改變一次。 然後重點來看下第二行程式碼:

handle(this, new Handler(onFulfilled, onRejected, prom));
複製程式碼

呼叫了handle函式,先不管handle做了什麼,我們先關注其第二個實參:

new Handler(onFulfilled, onRejected, prom)
複製程式碼

其例項化了Handler物件,引數為then方法的兩個引數和prom物件,我們來看下其具體實現:

/**
 * @constructor
 */
function Handler(onFulfilled, onRejected, promise) {
  this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
  this.onRejected = typeof onRejected === 'function' ? onRejected : null;
  this.promise = promise;
}
複製程式碼

Handler建構函式將傳入的引數分別賦值給例項物件的onFulfilled、onRejected、promise屬性,其中對onFulfilled和onRejected做了處理,若不是函式型別,則賦值為null。這說明,我們傳入給then方法的兩個引數可以不為函式型別,其內部會調整為null。 明白了第二個引數,我們來看handle函式具體做了什麼:

function handle(self, deferred) {
  while (self._state === 3) {
    self = self._value;
  }
  if (self._state === 0) {
    self._deferreds.push(deferred);
    return;
  }
  self._handled = true;
  Promise._immediateFn(function() {
    var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
    if (cb === null) {
      (self._state === 1 ? resolve : reject)(deferred.promise, self._value);
      return;
    }
    var ret;
    try {
      ret = cb(self._value);
    } catch (e) {
      reject(deferred.promise, e);
      return;
    }
    resolve(deferred.promise, ret);
  });
}
複製程式碼

首先是一個while迴圈:

 while (self._state === 3) {
    self = self._value;
  }
複製程式碼

self指的是當前Promise物件,如果self._state的值為3,則將self._value賦值給self。我們在上篇文章分析過,_state屬性值為3,則說明_value值為一個Promise物件。那麼這個迴圈的結果就是,直到_value屬性值不為Promise物件,為什麼要這麼處理呢?我們來看下規範是怎麼說的: 如果 x 為 Promise,則使promise接收x的狀態

  • 如果 x 處於pendding,promise需要保持為pendding狀態直至x被解決或拒絕
  • 如果 x 處於fulFilled,用相同的值執行 promise
  • 如果 x 處於rejected,用相同的據因拒絕 promise 總結起來就是,如果_value屬性值為Promise物件,則結果取決於巢狀最內層Promise的狀態。 接下來是一個條件判斷:
 if (self._state === 0) {
    self._deferreds.push(deferred);
    return;
  }
複製程式碼

如果self._state屬性為0,則將deferred壓入self._deferreds陣列,並結束此次函式呼叫。其中deferred為傳入的Handler例項物件,我們在上篇裡分析過,_state屬性值為0表示Promise的狀態為pendding,我們可以猜測到,狀態為pedding,也就是Promise的狀態並未改變,then方法不知道要執行哪個回撥,所在要先儲存。那麼為什麼是儲存在一個陣列裡,而不是儲存在一個變數裡,難道有很多個?其實還真可能有很多個,因為then方法可以被多次呼叫:

image.png
可以看到,每個then方法的回撥都被執行了。 再來看下面的程式碼:

self._handled = true;
複製程式碼

上篇文章也分析過,_handled屬性用來標記Promise是否被處理,這裡將其賦值為true,說明當前Promise物件已經被處理了。 最後來看最後一段程式碼:

Promise._immediateFn(function() {
   ...
});
複製程式碼

呼叫了Promise._immediateFn方法,並傳入了一個回撥函式。先來看Promise._immediateFn的定義:

// Use polyfill for setImmediate for performance gains
Promise._immediateFn =
  (typeof setImmediate === 'function' &&
    function(fn) {
      setImmediate(fn);
    }) ||
  function(fn) {
    setTimeoutFunc(fn, 0);
  };
複製程式碼

這裡判斷setImmediate是否是函式型別,成裡則賦值為function(fn) { setImmediate(fn) },否則賦值為function(fn) { setTimeoutFunc(fn, 0) },其中setTimeoutFunc是setTimeout的別名:

var setTimeoutFunc = setTimeout;
複製程式碼

setImmediate是Node.js裡的global物件的屬性,而setTimeout是瀏覽器環境裡window物件的屬性,所以Promise._immediate是相容兩個環境所做處理的程式碼。為什麼要再包一層閉包呢?應該是相容引數的數量。 到這我們也明白了,then方法的回撥是非同步執行,其實更具體是在micro佇列中,這裡我們就不展開了。 回到Promise._immediateFn的回撥引數:

var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
複製程式碼

上篇文章分析過,self._state屬性值為1表示Promise的狀態為已完成,為2表示狀態為被決絕。那麼這句程式碼的意思是,根據Promise的狀態,將then方法的完成回撥或決絕回撥賦值給cb變數。 再來看下面的條件判斷:

if (cb === null) {
  (self._state === 1 ? resolve : reject)(deferred.promise, self._value);
  return;
}
複製程式碼

cb變數為null,也就是我們傳入給then方法的引數不是函式型別,這裡會根據Promise的狀態執行resolve或reject函式,並結束此次呼叫。注意傳入的引數,deferred.promise和self._value,也就是說,用Promise的值去改變在then方法內建立的Promise物件的狀態。總結起來就是,若then方法未傳入對應的回撥,那麼Promise的值會被傳遞到下一次then方法中:

image.png

再來看最後一段程式碼:

var ret;
try {
  ret = cb(self._value);
} catch (e) {
  reject(deferred.promise, e);
  return;
}
resolve(deferred.promise, ret);
複製程式碼

忽略try..catch,核心是這樣的:

var ret = cb(self._value);
resolve(deferred.promise, ret);
複製程式碼

將self._value作為引數,呼叫cb函式,返回值儲存在ret變數中,再以ret變數為引數呼叫resolve函式。這裡的意思就是,將cb函式的返回值作為Promise的值傳遞給下一個then方法:

image.png
當然,若丟擲異常,則將原因作為Promise的值,傳遞給下一個then方法:

reject(deferred.promise, e);
return;
複製程式碼

至此,Promise原始碼的核心部分已經分析完了,我們可以發現,閱讀原始碼可以瞭解Promise的內部的工作機制,當出現問題時,我們也能快速定位原因。鼓勵大家去閱讀原始碼! 當然還有catch、all、race等方法,將在下一篇文章繼續分析。

相關文章