前言
前一陣子記錄了promise的一些常規(guī)用法蝎抽,這篇文章再深入一個層次,來分析分析promise的這種規(guī)則機制是如何實現(xiàn)的缚去。ps:本文適合已經(jīng)對promise的用法有所了解的人閱讀潮秘。
本文的promise源碼是按照Promise/A+規(guī)范來編寫的(不想看英文版的移步Promise/A+規(guī)范中文翻譯)
引子
為了讓大家更容易理解,我們從一個場景開始講解易结,讓大家一步一步跟著思路思考枕荞,相信你一定會更容易看懂。
考慮下面一種獲取用戶id的請求處理
//例1
function getUserId() {
return new Promise(function(resolve) {
//異步請求
http.get(url, function(results) {
resolve(results.id)
})
})
}
getUserId().then(function(id) {
//一些處理
})
getUserId
方法返回一個promise
搞动,可以通過它的then
方法注冊(注意注冊
這個詞)在promise
異步操作成功時執(zhí)行的回調躏精。這種執(zhí)行方式,使得異步調用變得十分順手鹦肿。
原理剖析
那么類似這種功能的Promise
怎么實現(xiàn)呢矗烛?其實按照上面一句話,實現(xiàn)一個最基礎的雛形還是很easy的箩溃。
極簡promise雛形
function Promise(fn) {
var value = null,
callbacks = []; //callbacks為數(shù)組瞭吃,因為可能同時有很多個回調
this.then = function (onFulfilled) {
callbacks.push(onFulfilled);
};
function resolve(value) {
callbacks.forEach(function (callback) {
callback(value);
});
}
fn(resolve);
}
上述代碼很簡單,大致的邏輯是這樣的:
調用
then
方法涣旨,將想要在Promise
異步操作成功時執(zhí)行的回調放入callbacks
隊列虱而,其實也就是注冊回調函數(shù),可以向觀察者模式方向思考开泽;創(chuàng)建
Promise
實例時傳入的函數(shù)會被賦予一個函數(shù)類型的參數(shù)牡拇,即resolve
,它接收一個參數(shù)value穆律,代表異步操作返回的結果惠呼,當一步操作執(zhí)行成功后,用戶會調用resolve
方法峦耘,這時候其實真正執(zhí)行的操作是將callbacks
隊列中的回調一一執(zhí)行剔蹋;
可以結合例1
中的代碼來看,首先new Promise
時辅髓,傳給promise
的函數(shù)發(fā)送異步請求泣崩,接著調用promise
對象的then
屬性少梁,注冊請求成功的回調函數(shù),然后當異步請求發(fā)送成功時矫付,調用resolve(results.id)
方法, 該方法執(zhí)行then
方法注冊的回調數(shù)組凯沪。
相信仔細的人應該可以看出來,then
方法應該能夠鏈式調用买优,但是上面的最基礎簡單的版本顯然無法支持鏈式調用妨马。想讓then
方法支持鏈式調用,其實也是很簡單的:
this.then = function (onFulfilled) {
callbacks.push(onFulfilled);
return this;
};
see?只要簡單一句話就可以實現(xiàn)類似下面的鏈式調用:
// 例2
function getUserId() {
return new Promise(function (resolve) {
resolve(9876);
});
}
getUserId().then(function (id) {
// 一些處理
}).then(function (id) {
// 一些處理
});
加入延時機制
細心的同學應該發(fā)現(xiàn)杀赢,上述代碼可能還存在一個問題:如果在then
方法注冊回調之前烘跺,resolve
函數(shù)就執(zhí)行了,怎么辦脂崔?比如promise
內(nèi)部的函數(shù)是同步函數(shù):
// 例3
function getUserId() {
return new Promise(function (resolve) {
resolve(9876);
});
}
getUserId().then(function (id) {
// 一些處理
});
這顯然是不允許的滤淳,Promises/A+
規(guī)范明確要求回調需要通過異步方式執(zhí)行,用以保證一致可靠的執(zhí)行順序砌左。因此我們要加入一些處理脖咐,保證在resolve
執(zhí)行之前,then
方法已經(jīng)注冊完所有的回調绊困。我們可以這樣改造下resolve
函數(shù):
function resolve(value) {
setTimeout(function() {
callbacks.forEach(function (callback) {
callback(value);
});
}, 0)
}
上述代碼的思路也很簡單文搂,就是通過setTimeout
機制适刀,將resolve
中執(zhí)行回調的邏輯放置到JS
任務隊列末尾秤朗,以保證在resolve
執(zhí)行時,then
方法的回調函數(shù)已經(jīng)注冊完成.
但是笔喉,這樣好像還存在一個問題取视,可以細想一下:如果Promise
異步操作已經(jīng)成功,這時常挚,在異步操作成功之前注冊的回調都會執(zhí)行作谭,但是在Promise
異步操作成功這之后調用的then
注冊的回調就再也不會執(zhí)行了,這顯然不是我們想要的奄毡。
加入狀態(tài)
恩折欠,為了解決上一節(jié)拋出的問題,我們必須加入狀態(tài)機制吼过,也就是大家熟知的pending
锐秦、fulfilled
、rejected
盗忱。
Promises/A+
規(guī)范中的2.1Promise States
中明確規(guī)定了酱床,pending
可以轉化為fulfilled
或rejected
并且只能轉化一次,也就是說如果pending
轉化到fulfilled
狀態(tài)趟佃,那么就不能再轉化到rejected
扇谣。并且fulfilled
和rejected
狀態(tài)只能由pending
轉化而來昧捷,兩者之間不能互相轉換。一圖勝千言:
改進后的代碼是這樣的:
function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];
this.then = function (onFulfilled) {
if (state === 'pending') {
callbacks.push(onFulfilled);
return this;
}
onFulfilled(value);
return this;
};
function resolve(newValue) {
value = newValue;
state = 'fulfilled';
setTimeout(function () {
callbacks.forEach(function (callback) {
callback(value);
});
}, 0);
}
fn(resolve);
}
上述代碼的思路是這樣的:resolve
執(zhí)行時罐寨,會將狀態(tài)設置為fulfilled
靡挥,在此之后調用then
添加的新回調,都會立即執(zhí)行衩茸。
這里沒有任何地方將state
設為rejected
芹血,為了讓大家聚焦在核心代碼上,這個問題后面會有一小節(jié)專門加入楞慈。
鏈式Promise
那么這里問題又來了幔烛,如果用戶再then函數(shù)里面注冊的仍然是一個Promise
,該如何解決囊蓝?比如下面的例4
:
// 例4
getUserId()
.then(getUserJobById)
.then(function (job) {
// 對job的處理
});
function getUserJobById(id) {
return new Promise(function (resolve) {
http.get(baseUrl + id, function(job) {
resolve(job);
});
});
}
這種場景相信用過promise
的人都知道會有很多饿悬,那么類似這種就是所謂的鏈式Promise
。
鏈式Promise
是指在當前promise
達到fulfilled
狀態(tài)后聚霜,即開始進行下一個promise
(后鄰promise
)狡恬。那么我們?nèi)绾毋暯赢斍?code>promise和后鄰promise
呢?(這是這里的難點)蝎宇。
其實也不是辣么難弟劲,只要在then
方法里面return
一個promise
就好啦。Promises/A+
規(guī)范中的2.2.7就是這么說噠(微笑臉)~
下面來看看這段暗藏玄機的then
方法和resolve
方法改造代碼:
function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];
this.then = function (onFulfilled) {
return new Promise(function (resolve) {
handle({
onFulfilled: onFulfilled || null,
resolve: resolve
});
});
};
function handle(callback) {
if (state === 'pending') {
callbacks.push(callback);
return;
}
//如果then中沒有傳遞任何東西
if(!callback.onFulfilled) {
callback.resolve(value);
return;
}
var ret = callback.onFulfilled(value);
callback.resolve(ret);
}
function resolve(newValue) {
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if (typeof then === 'function') {
then.call(newValue, resolve);
return;
}
}
state = 'fulfilled';
value = newValue;
setTimeout(function () {
callbacks.forEach(function (callback) {
handle(callback);
});
}, 0);
}
fn(resolve);
}
我們結合例4
的代碼姥芥,分析下上面的代碼邏輯兔乞,為了方便閱讀,我把例4
的代碼貼在這里:
// 例4
getUserId()
.then(getUserJobById)
.then(function (job) {
// 對job的處理
});
function getUserJobById(id) {
return new Promise(function (resolve) {
http.get(baseUrl + id, function(job) {
resolve(job);
});
});
}
then
方法中凉唐,創(chuàng)建并返回了新的Promise
實例庸追,這是串行Promise
的基礎,并且支持鏈式調用台囱。handle
方法是promise
內(nèi)部的方法淡溯。then
方法傳入的形參onFulfilled
以及創(chuàng)建新Promise
實例時傳入的resolve
均被push
到當前promise
的callbacks
隊列中,這是銜接當前promise
和后鄰promise
的關鍵所在(這里一定要好好的分析下handle的作用)簿训。getUserId
生成的promise
(簡稱getUserId promise
)異步操作成功咱娶,執(zhí)行其內(nèi)部方法resolve
,傳入的參數(shù)正是異步操作的結果id
調用
handle
方法處理callbacks
隊列中的回調:getUserJobById
方法强品,生成新的promise
(getUserJobById promise
)執(zhí)行之前由
getUserId promise
的then
方法生成的新promise
(稱為bridge promise
)的resolve
方法膘侮,傳入?yún)?shù)為getUserJobById promise
。這種情況下择懂,會將該resolve
方法傳入getUserJobById promise
的then
方法中喻喳,并直接返回。在
getUserJobById promise
異步操作成功時困曙,執(zhí)行其callbacks
中的回調:getUserId bridge promise
中的resolve
方法-
最后執(zhí)行
getUserId bridge promise
的后鄰promise
的callbacks
中的回調表伦。
更直白的可以看下面的圖谦去,一圖勝千言(都是根據(jù)自己的理解畫出來的,如有不對歡迎指正):
失敗處理
在異步操作失敗時蹦哼,標記其狀態(tài)為rejected
鳄哭,并執(zhí)行注冊的失敗回調:
//例5
function getUserId() {
return new Promise(function(resolve) {
//異步請求
http.get(url, function(error, results) {
if (error) {
reject(error);
}
resolve(results.id)
})
})
}
getUserId().then(function(id) {
//一些處理
}, function(error) {
console.log(error)
})
有了之前處理fulfilled
狀態(tài)的經(jīng)驗,支持錯誤處理變得很容易,只需要在注冊回調纲熏、處理狀態(tài)變更上都要加入新的邏輯:
function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];
this.then = function (onFulfilled, onRejected) {
return new Promise(function (resolve, reject) {
handle({
onFulfilled: onFulfilled || null,
onRejected: onRejected || null,
resolve: resolve,
reject: reject
});
});
};
function handle(callback) {
if (state === 'pending') {
callbacks.push(callback);
return;
}
var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
ret;
if (cb === null) {
cb = state === 'fulfilled' ? callback.resolve : callback.reject;
cb(value);
return;
}
ret = cb(value);
callback.resolve(ret);
}
function resolve(newValue) {
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if (typeof then === 'function') {
then.call(newValue, resolve, reject);
return;
}
}
state = 'fulfilled';
value = newValue;
execute();
}
function reject(reason) {
state = 'rejected';
value = reason;
execute();
}
function execute() {
setTimeout(function () {
callbacks.forEach(function (callback) {
handle(callback);
});
}, 0);
}
fn(resolve, reject);
}
上述代碼增加了新的reject
方法妆丘,供異步操作失敗時調用,同時抽出了resolve
和reject
共用的部分局劲,形成execute
方法勺拣。
錯誤冒泡是上述代碼已經(jīng)支持,且非常實用的一個特性鱼填。在handle
中發(fā)現(xiàn)沒有指定異步操作失敗的回調時药有,會直接將bridge promise
(then
函數(shù)返回的promise
,后同)設為rejected
狀態(tài)苹丸,如此達成執(zhí)行后續(xù)失敗回調的效果愤惰。這有利于簡化串行Promise
的失敗處理成本,因為一組異步操作往往會對應一個實際功能赘理,失敗處理方法通常是一致的:
//例6
getUserId()
.then(getUserJobById)
.then(function (job) {
// 處理job
}, function (error) {
// getUserId或者getUerJobById時出現(xiàn)的錯誤
console.log(error);
});
異常處理
細心的同學會想到:如果在執(zhí)行成功回調宦言、失敗回調時代碼出錯怎么辦?對于這類異常商模,可以使用try-catch
捕獲錯誤奠旺,并將bridge promise
設為rejected
狀態(tài)。handle
方法改造如下:
function handle(callback) {
if (state === 'pending') {
callbacks.push(callback);
return;
}
var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
ret;
if (cb === null) {
cb = state === 'fulfilled' ? callback.resolve : callback.reject;
cb(value);
return;
}
try {
ret = cb(value);
callback.resolve(ret);
} catch (e) {
callback.reject(e);
}
}
如果在異步操作中阻桅,多次執(zhí)行resolve
或者reject
會重復處理后續(xù)回調凉倚,可以通過內(nèi)置一個標志位解決兼都。
總結
剛開始看promise源碼的時候總不能很好的理解then和resolve函數(shù)的運行機理嫂沉,但是如果你靜下心來,反過來根據(jù)執(zhí)行promise時的邏輯來推演扮碧,就不難理解了趟章。這里一定要注意的點是:promise里面的then函數(shù)僅僅是注冊了后續(xù)需要執(zhí)行的代碼,真正的執(zhí)行是在resolve方法里面執(zhí)行的慎王,理清了這層蚓土,再來分析源碼會省力的多。
現(xiàn)在回顧下Promise的實現(xiàn)過程赖淤,其主要使用了設計模式中的觀察者模式:
通過Promise.prototype.then和Promise.prototype.catch方法將觀察者方法注冊到被觀察者Promise對象中蜀漆,同時返回一個新的Promise對象,以便可以鏈式調用咱旱。
被觀察者管理內(nèi)部pending确丢、fulfilled和rejected的狀態(tài)轉變绷耍,同時通過構造函數(shù)中傳遞的resolve和reject方法以主動觸發(fā)狀態(tài)轉變和通知觀察者。