老生常談:Promise 用法與原始碼分析
此文章是幾個月前寫得,發現沒有發表過,就在此發表一下。
背景
Promise本身是一個非同步程式設計的方案,讓處理過程變得更簡單。es6引入promise特性來處理JavaScript中的非同步場景。以前,處理非同步最常用的方法就是回撥函式,但是當過程稍微複雜一點,多個非同步操作集中在一起的時候,就容易出現一個回撥金字塔的情況,可讀性和可維護性都非常差,比如:
setTimeout(function () {
console.log('ping');
setTimeout(function () {
console.log('pong');
setTimeout(function () {
console.log('end!');
}, 1000);
}, 1000);
}, 1000);
複製程式碼
Promise可以避免這種情況發生,將回撥巢狀轉變為鏈式呼叫,避免回撥金字塔的出現。
Promise基本用法
Promise有4種狀態:
- fulfilled——成功狀態
- rejected——失敗狀態
- pending——執行狀態(未成功也未失敗)
- settled——完成狀態
let promise = new Promise((resolve, reject) => {
// when success, resolve
let value = 'success';
resolve(value);
// when an error occurred, reject
reject(new Error('Something happened!'));
});
複製程式碼
可以通過then方法來處理返回的結果
// promise.then(onResolve, onReject)
promise.then(response => {
console.log(response);
}, error => {
console.log(error);
});
複製程式碼
then方法不僅僅是處理結果,而且還可以繼續返回promise物件
promise.then(response => {
console.log(response); // success
return 'another success';
}).then(response => {
console.log(response); // another success
});
複製程式碼
對reject狀態返回的結果的處理,可以通過then的第二個引數,也可以通過catch方法
promise.then(
null,
error => {
console.log(error); // failure
}
);
// 或
promise.catch(err => {
console.log(err); // failure
});
複製程式碼
同時處理多個promise,不關注執行順序可以用all方法
let doSmth = new Promise(resolve => {
resolve('doSmth');
}),
doSmthElse = new Promise(resolve => {
resolve('doSmthElse');
}),
oneMore = new Promise(resolve => {
resolve('oneMore');
});
Promise.all([
doSmth,
doSmthElse,
oneMore
])
.then(response => {
let [one, two, three] = response;
console.log(one, two, three); // doSmth doSmthElse oneMore
});
複製程式碼
Promise.all()接收一個promises陣列,當全部fulfiled時,返回一個按順序的陣列 當其中一個reject時,返回第一個rejected的值 或者race方法,接收多個promise例項,組成一個新的promise,有一個變化的時候,外層promise跟著變化。 快捷方法:
- Promise.resolve(value) 返回一個resolve(value)的promise 或直接返回這個value如果value本身時promise物件的話。
- Promise.reject(value) 返回一個rejected狀態的promise,並且reject(value)
——參考ES6 Promise
原理
為了學習promise內部原理,最好是看其實現原始碼,then/promise是github上一個遵循promise A+規範的庫,其核心程式碼在core檔案中。那麼就從這個庫來學習。
function noop() {} // 定義一個空函式用於對比和例項化空promise,後面會用到
// States:
// 庫定義的4種狀態
// 0 - pending
// 1 - fulfilled with _value
// 2 - rejected with _value
// 3 - adopted the state of another promise, _value
var LAST_ERROR = null;
var IS_ERROR = {}; // 這兩個用來捕獲錯誤
// 獲取obj中的then方法
function getThen(obj) {
try {
return obj.then;
} catch (ex) {
LAST_ERROR = ex;
return IS_ERROR;
}
}
// 當then中只傳進了一個回撥函式時呼叫此方法
function tryCallOne(fn, a) {
try {
return fn(a);
} catch (ex) {
LAST_ERROR = ex;
return IS_ERROR;
}
}
// 當then中傳入了兩個回撥函式時呼叫此方法
function tryCallTwo(fn, a, b) {
try {
fn(a, b);
} catch (ex) {
LAST_ERROR = ex;
return IS_ERROR;
}
}
複製程式碼
// Promise建構函式
function Promise(fn) {
// 檢驗是否例項化了promise物件,不能直接使用promise建構函式來封裝自己的程式碼
if (typeof this !== 'object') {
throw new TypeError('Promises must be constructed via new');
}
// 檢驗傳進來的是否為函式,promise必須接受一個函式來進行例項化
if (typeof fn !== 'function') {
throw new TypeError('Promise constructor\'s argument is not a function');
}
this._deferredState = 0;
// 與後面的this._deferreds關係密切,當resolve方法接收的是一個promise時,回用到他們
this._state = 0; // 對應上方4種狀態
this._value = null; // 存放最終結果
this._deferreds = null; // 存放then中接收的處理函式
if (fn === noop) return; // 如果promise接收的是空函式,直接返回,結束。
doResolve(fn, this);
}
複製程式碼
可以看到,我通過Promise建構函式例項化一個promise物件,在對引數進行檢查後,我們會執行doResolve(fn, this)方法,順藤摸瓜看看doResolve函式做了什麼
function doResolve(fn, promise) {
var done = false; // 確保onFulfilled 和 onRejected只被呼叫一次
var res = tryCallTwo(fn, function (value) {
if (done) return;
done = true;
resolve(promise, value);
}, function (reason) {
if (done) return;
done = true;
reject(promise, reason);
});
if (!done && res === IS_ERROR) {
done = true;
reject(promise, LAST_ERROR);
}
}
複製程式碼
這裡就是將兩個回撥函式分別傳給 fn 的 兩個引數,並確保他們只執行一次。 接下來就要看它的resolve方法。
function resolve(self, newValue) {
if (newValue === self) {
return reject(
self,
new TypeError('A promise cannot be resolved with itself.')
);
}
if (
newValue &&
(typeof newValue === 'object' || typeof newValue === 'function')
) {
var then = getThen(newValue);
if (then === IS_ERROR) {
return reject(self, LAST_ERROR);
}
if (
then === self.then &&
newValue instanceof Promise
) {
// 當接收的引數為promise,或thenable物件時。
self._state = 3;
self._value = newValue;
finale(self); // 執行_deferreds 中的方法,如果有的話。
return;
} else if (typeof then === 'function') {
doResolve(then.bind(newValue), self);
return;
}
}
self._state = 1;
self._value = newValue;
finale(self);
}
複製程式碼
resolve除了一些判斷外,就是根據接收到的引數的型別來修改state的值。如果接收到promise物件或thenable物件,state轉為3,並使用它的結果,如果時其他如字串型別等,state轉為1,直接使用該值。 有了結果之後,就要看一下then方法了。
Promise.prototype.then = function(onFulfilled, onRejected) {
if (this.constructor !== Promise) {
return safeThen(this, onFulfilled, onRejected);
}
var res = new Promise(noop);
handle(this, new Handler(onFulfilled, onRejected, res));
return res;
};
function safeThen(self, onFulfilled, onRejected) {
return new self.constructor(function (resolve, reject) {
var res = new Promise(noop);
res.then(resolve, reject);
handle(self, new Handler(onFulfilled, onRejected, res));
});
}
複製程式碼
then方法也很簡單,就是用Handler包裝一個物件
function Handler(onFulfilled, onRejected, promise){
this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
this.onRejected = typeof onRejected === 'function' ? onRejected : null;
this.promise = promise;
}
複製程式碼
然後呼叫handle方法。整個過程就是建立一個新的promise,呼叫handle方法,將新的promise返回,以便實現鏈式呼叫。 下面看一下handle方法。
function handle(self, deferred) {
// self 移動指向最新的promise
while (self._state === 3) {
self = self._value;
}
if (Promise._onHandle) {
Promise._onHandle(self);
}
if (self._state === 0) {
// 向_deferredState中新增handler處理過得物件,也就是{onFulfilled,onRejected,promise}
if (self._deferredState === 0) {
self._deferredState = 1;
self._deferreds = deferred;
return;
}
if (self._deferredState === 1) {
self._deferredState = 2;
self._deferreds = [self._deferreds, deferred];
return;
}
self._deferreds.push(deferred);
return;
}
handleResolved(self, deferred);
}
複製程式碼
handle方法就是根據state的值和_deferredState ,來決定要做的事情,我們來捋一捋,當我們的resolve方執行,state轉為1時,我們會進入then方法,然後進入handle方法,因為state為1,可以看到我們會直接進入handleResolved方法。
resolve -> then -> handle -> handleResolved
複製程式碼
看看handleResolved函式是做什麼的
function handleResolved(self, deferred) {
asap(function() {
var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
if (cb === null) {
if (self._state === 1) {
resolve(deferred.promise, self._value);
} else {
reject(deferred.promise, self._value);
}
return;
}
var ret = tryCallOne(cb, self._value);
if (ret === IS_ERROR) {
reject(deferred.promise, LAST_ERROR);
} else {
// 此處主要服務於promise的鏈式呼叫,因為promise通過返回一個新的promise來實現鏈式呼叫。
// 新的promise儲存在deferred.promise中
resolve(deferred.promise, ret);
}
});
}
複製程式碼
過濾掉新增判斷,handleResolved就是使用結果值self._value呼叫then中的相應回撥(成功或失敗)。 那當resolve接收的是普通值得時候整個執行過程就知道了。
resolve -> then -> handle -> handleResolved -> 執行onFulfilled或onRejected
複製程式碼
當我們resolve接收到得是一個promise或thenable物件時,我們進入到handle後,會進入while迴圈,直到self指向接收到的promise,以接收到的promise的結果為標準,在接收到的promise的 state===0 階段我們會將原始promise中拿到得onFulfilled以及onRejected回撥方法(包含在deferred物件中),新增到接收到的promise的 _deferreds 中,然後return。 存在 _deferreds 中的回撥在什麼時候執行呢? 我們可以看到無論時resolve還是reject,只要狀態改變都會執行 finale 方法,我們看一下 finale
function finale(self) {
if (self._deferredState === 1) {
handle(self, self._deferreds);
self._deferreds = null;
}
if (self._deferredState === 2) {
for (var i = 0; i < self._deferreds.length; i++) {
handle(self, self._deferreds[i]);
}
self._deferreds = null;
}
}
複製程式碼
因為每次執行此方法都是在state狀態改變的時候,所以進入handle函式後會直接進入handleResolved方法,然後使用self._value的結果值執行對應的回撥函式(onFulfilled 或 onRejected)。 最後看看reject
function reject(self, newValue) {
self._state = 2;
self._value = newValue;
if (Promise._onReject) {
Promise._onReject(self, newValue);
}
finale(self);
}
複製程式碼
這下清晰多了,再來捋一捋,
總結
- Promise本身是一個非同步程式設計的方案,讓處理過程變得更簡單。es6引入promise特性來處理JavaScript中的非同步場景,代替了傳統基於回撥的方案,防止瞭如回撥金字塔等現象的發生。
- promise內部執行機制:使用promise封裝非同步函式,通過resolve和reject方法來處理結果,
- 當發生錯誤時,reject會將state轉為狀態2(rejected)並呼叫對應的onRejected回撥函式,
- 當成功時,resolve接收對應的結果,當結果時普通值(比如string型別)他會將state轉為狀態1,直接使用該值呼叫對應的onFulfilled回撥函式,
- 當接收到的是一個promise物件或者thenable物件時,會將thenable物件轉為promise物件,並將當前state轉為3,將我們的onFulfilled和onRejected回掉函式儲存到接收到的promise中,並採用接收到的promise的結果為最終標準,當它的state發生變化時,執行相應的回撥函式。
- 其鏈式呼叫時通過返回一個新的promise空物件來實現的,在當前的onFulfilled或onRejected回撥執行後,會將執行結果以及新的promise作為引數去呼叫onFulfilled或onRejected方法,實現值在鏈式中的傳遞。