平時在專案中經常使用到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的狀態。如下:
總結:Promise建構函式除了執行了我們傳入的回撥函式,還會儲存Promise的狀態以及相應的完成的值或拒絕的原因。
最後,祝大家中秋快樂!