前言
距離我上次寫的那篇Promise實現的基本原理(一)有十幾天了(太懶了),這篇我想寫Promise是如何實現串聯呼叫then,解決回撥地獄的。雖然說《深入理解ES6》這本書把Promise用法講得很詳細,但是自己理解起來還是有很多疑問(我好想上一篇也是這麼說的)。
參考連結:JavaScript Promises ... In Wicked Detail
github上的完整實現:Bare bones Promises/A+ implementation
一、Promise可能會出現的非同步情況
情況一:對resolve的呼叫是非同步的
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('resolve')
console.log('Promise setTimeout')
}, 1000)
}).then(result => {
// 這個箭頭函式我們叫 fn1
console.log(result)
}, error => {
// 這個箭頭函式我們叫 fn2
console.log(error)
})
複製程式碼
情況二:對then的呼叫是非同步的
let p1 = new Promise((resolve, reject) => {
resolve('resolve')
console.log('Promise setTimeout')
})
setTimeout(() => {
p1.then(result => {
// 這個箭頭函式我們叫 fn1
console.log(result)
}, error => {
// 這個箭頭函式我們叫 fn2
console.log(error)
})
}, 1000)
複製程式碼
情況三:呼叫resolve和呼叫then都是非同步的
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('resolve')
console.log('Promise setTimeout')
}, 1000)
})
setTimeout(() => {
p2.then(result => {
// 這個箭頭函式我們叫 fn1
console.log(result)
}, error => {
// 這個箭頭函式我們叫 fn2
console.log(error)
})
}, 1000)
複製程式碼
第三種情況是前兩種情況的結合,如果第一個setTimeout
時間比第二個setTimeout
時間長,那就是第一種情況;如果第二個setTimeout
比第一個setTimeout
的時間長,那就是第二種情況。瞭解event-loop
的同學看著幾種情況應該比較清晰,不瞭解的同學就看我下面的分析。
二、Promise三種狀態是如何工作的
我從《深入理解ES6》Promise
那章擷取了一下內容:
每個
Promise
都會經歷一個短暫的生命週期:先是處於進行中(padding
)的狀態,此時操作尚未完成,所以他也是未處理(unsettled
)的;一旦非同步操作執行結束,Promise
則變為已處理(settled
)的狀態,操作結束後Promise
可能會進入到以下兩個狀態的其中一個:
- Fulfilled
Promise
非同步操作成功完成- Rejected 由於程式錯誤或者一些其他原因,
Promise
非同步操作未能成功完成
Promise
的內部屬性[[PromiseState]]
被用來表示Promise
的3中狀態:"pedding"
、"fulfilled"
、"rejected"
。
實現程式碼:
function Promise(fn) {
var state = 'pending'; // 初始狀態為padding
var value; // Promise處於被處理狀態下儲存下來的值
var deferred = null; // 延期執行函式(現在還看不出來它是函式)
/**
* 非同步操作成功完成回撥函式
* 在new Promise的時候被呼叫的(不清楚為什麼的可以看上一篇部落格)
*/
function resolve(newValue) {
// 如果在then的回撥函式裡面返回了一個Promise
if(newValue && typeof newValue.then === 'function') {
// 將這個Promise的then的兩個回撥引數的引用指向Promise的resolve和reject
newValue.then(resolve, reject);
return;
}
// 如果傳進來的不是Promise
state = 'resolved'; // 將Promise的狀態改成resolved(fulfilled)
value = newValue; // 儲存return過來的值
if(deferred) { // 判斷是否存在傳進來的回撥函式
handle(deferred); // 如果有,將我們傳進來的回撥函式交給handle
} //(不清楚為什麼的可以看上一篇部落格)
}
/**
* 非同步操作失敗/被拒絕回撥函式
* 在 new Promise 的時候被呼叫的
*/
function reject(reason) {
state = 'rejected'; // 將Promise的狀態改成rejected
value = reason; // 儲存我們傳進來的被拒絕原因
if(deferred) { // 和 resolve同理
handle(deferred);
}
}
function handle(handler) {
// handle被呼叫了,但是此時promise的狀態還是處於出事的未處理狀態
if(state === 'pending') {
deferred = handler; // 就將handler的引用先儲存下來
return; // 不執行下面的程式碼
}
var handlerCallback; // Promise處理完成的狀態只能是一種(fulfilled/rejected),所以用同一個變數儲存回撥的引用
if(state === 'resolved') { // 如果已經成功處理(fulfilled)
handlerCallback = handler.onResolved; // 引用是處理成功回撥的引用
} else {
handlerCallback = handler.onRejected; // 如果不是,引用是處理失敗回撥的引用
}
if(!handlerCallback) { // 如果在呼叫then的時候沒有給then傳回撥函式,即上一個if語句儲存下來的引用是undefined的
if(state === 'resolved') { // 如果狀態已經是resolved(fulfilled)的
handler.resolve(value); // 就呼叫hander的resolved(下一個then傳入的成功回撥函式)
} else {
handler.reject(value); // 否則,呼叫handler的reject(下一個then傳入的第二個引數,即失敗回撥函式)
}
return; // 不執行下面的語句
}
// 如果本次呼叫的then有傳入回撥函式
var ret = handlerCallback(value);
handler.resolve(ret); // 就給
}
this.then = function(onResolved, onRejected) {
return new Promise(function fn3 (resolve, reject) { // then 返回一Promise
// 返回的Promise把then傳進來的兩個回撥和本省的兩個回撥都丟給handle處理
handle({
onResolved: onResolved,
onRejected: onRejected,
resolve: resolve,
reject: reject
});
});
};
fn(resolve, reject);
}
複製程式碼
回撥函式引用分析:
fn函式的引用在上一篇已經分析過了,這一片重點分析在then中返回Promise給handle函式傳過去的物件屬性引用
onResolved
: 在我們用Promise
物件呼叫then
的時候傳過來的第一個引數 fn1 (我在註釋裡給了個名字好區分,就不像第一篇那樣一個一個拆出來了,有點懶)。onReject
: 在我們用Promise
物件呼叫then
的時候傳過來的第二個引數fn2。resolve
:then
返回的Promise
物件傳過去的fn3
, 當fn3
被呼叫時Promise
傳給它的第一個引數就是resolve
的引用,所以resolve
(這是key) 的引用是Promise
的內部函式resolve
(這是function)。當然,每一次new Promise
都會是一個新的物件,所以內部的resolve
的引用也是不一樣的。reject
:reject
和resolve
同理,指向Promise
內部函式reject
。
handle
函式分析
1、handle
引數分析
handle
函式的呼叫還是在使用者呼叫then
的時候呼叫,只不過這次then
函式返回了一個Promise
,給handle
函式傳入的值既保留了本次呼叫then
傳入的兩個回撥函式的引用,也保留了新的Promise
的回撥函式引用。
2、結合三種狀態分析
(1)"pending"
狀態:
當呼叫handle
函式時Promise
處於padding
對應的情況是情況一,此時使用者建立了Promise
物件後立即呼叫了then
函式,但resolve
還沒有被處理,所以Promise
狀態沒有改變,then
的回撥函式需要等待resolve
被處理了才可以執行。所以當padding
狀態的時候,將handler
這個引數交給deferred
暫存。
(2)"resolved"
/rejected
狀態:
一秒後,resolve
回撥函式被呼叫了(關於resolve
的指向可以看第一篇),Promise
的state
被置為"resolved"
了,然後只要deferred
有指向,就再一次交給handle
函式處理一次。
此時Promise
的狀態是"resolved"
,將handlerCallback
的引用指向fn1,又將handleCallback
返回的值儲存在了ret
中,同時又傳給then
返回的Promise
的resolve
,讓返回的Promise
處於已處理狀態(只有當resolve
被執行了,then
的回撥函式才會執行,才能夠實現then
的無限串聯呼叫),也就是一下這種情況:
let p = new Promise((resolve, reject) => {
resolve(42)
})
p.then().then().then(result => {
console.log(result) // 控制檯輸出42
})
複製程式碼
三、如何實現then
的串聯使用
上面那一點其實就可以看出,Promise
是通過在then
函式裡面返回了一個Promise
,實現了Promise
的then
的串聯使用。但上面的那情況並沒有真正體現Promise
的作用,請看以下程式碼:
new Promise((resolve, result) => { // Promise1
resolve(1)
}).then(result => { // Promise1的then
console.log(result) // 1
return new Promise((resolve, reject) => { // Promise3
resolve(2)
})
}) // 此處得到Promise2
.then(result => { // Promise2的then
console.log(result) // 2
return 3
}) // 此處得到Promise4
.then(result => { // Promise4的then
console.log(result) // 3
}) // 此處得到Promise5
複製程式碼
在實現程式碼裡面可以看到then
時返回了一個新的Promise
的,然後再看上面的程式碼,在呼叫then
的第一個回撥函式回撥的時候,我們給他返回了一個引數,一種是Promise
物件,一種是返回一個數值,到這裡我的疑問是then
內部返回的Promise
是如何拿到這些值的,以下分析?上面的程式碼
Promise個數分析
上面那段程式碼裡裡外外有5個Promise,先起個小名,分別是:
new
的時候有一個 —— Promise1- Promise1的
then
內部返回了一個 —— Promise2 - Promise1的
then
的第一個回撥函式返回了一個 —— Promise3 - Promise2的
then
內部返回了一個 —— Promise4 - Promise4的
then
內部返回了一個 —— Promise5
1、Promise1:
handler
引數四個屬性指向:
onResolved
: Promise1的then
的第一個引數(回撥函式)onRejected
: Promise1的then
的第二個引數(回撥函式)resolve
: Promise2的內部函式resolve
reject
: Promise2的內部函式reject
Promise1當前的狀態是"resolved"
then
被呼叫,所以handle
被呼叫blablabla~
handleCallback
指向handler.onResolved
各種判斷賦值後到了handle
的最後兩行,此時ret
接收到handleCallback
返回的值,然後這個值是Promise3。接著呼叫了handle.resolve
,並且給它傳了ret
,而handle.resolve
指向的是Promise2的resolve
,所以接下來就跳到Promise2了。
2、Promise2:
handler
引數四個屬性指向:
onResolved
: Promise2的內部函式resolve
onRejected
: Promise2的內部函式reject
resolve
: Promise4的內部函式resolve
reject
: Promise4的內部函式reject
Promise2的resolve
在Promise1的handle
函式的最後一條語句被呼叫,並且傳入的ret
是一個Promise
(Promise3)
所以Promise2的resolve
函式中的第一個語句成立,但Promise2的狀態不改變,newValue
指向的是Promise3,需要等待Promise3被處理。
接著Promise3的then
被呼叫,所以接下要調到Promise3了。
3、Promise3
handler
引數四個屬性指向:
onResolved
: Promise2的內部函式resolve
onRejected
: Promise2的內部函式reject
resolve
: Promise3的then
返回的Promise
的內部函式resolve
(在當前例子不是很重要,忽略)reject
: Promise3的then
返回的Promise
的內部函式reject
(在當前例子不是很重要,忽略)
在Promise2呼叫Promise3的then
的時候,Promise3的狀態未知,但不管怎樣,只要Promise
的狀態改變了,就會呼叫一次handle函式。
某個時刻Promise3的狀態變為了"resolved"
/"rejected"
或者在Promise2呼叫它的時候就已經是"resolved"
/"rejected"
狀態
還是到handl
e函式的最後兩行,和Promise1的一樣,handler.onResolved
會被執行,並且把Promise3的resolve
回撥時得到的值value
傳給Promise2
的內部函式resolve
,Promise2變為了"resolved"
狀態,並且的到了Promise3被處理後的value
。
Promise3此時的任務已經完成了,被處理了,並且把得到的value
傳給了Promise2,拜拜?。
4、回到Promise2
handler
引數四個屬性的指向還是和之前一樣,在Promise3被處理之後,Promise2的狀態也會跟著Promise3的狀態變化,並且拿到了Promise3的value
,所以先在Promise2的value
是2
Promise2狀態變為已處理之後,不管什麼時候呼叫then
,then
的兩個回撥函式其中一個會被回撥,所以Promise
的這種處理,會讓我這種菜?覺得then
的回撥函式裡面return
的Promise
是變成了then
的return
,但其實Promise做的事情只是儲存值而已。
5、Promise4
Promise4的handle
r引數屬性指向其實和Promise2是差不多的,和Promise2不同的是:ret
的到不是一個Promise
,而是一個具體的值。也就是說newValue = 3
,newValue不是Promise
就好辦了,把狀態改成"resolved"
,儲存一下return
過來的值,其他和最開始的Promise
處理方式一樣,完事。
總結
Promise
能夠串聯呼叫then
,還是儲存值和各種引用的轉換,Promise
大法好!