3.3 Promise 信任問題
回顧一下只用回調(diào)編碼的信任問題砸讳,把一個(gè)回調(diào)傳入工具foo()時(shí)可能出現(xiàn)如下問題:
- 調(diào)用回調(diào)過早
- 調(diào)用回調(diào)過晚(或不被調(diào)用)
- 調(diào)用回調(diào)次數(shù)過少或過多
- 未能傳遞所需的環(huán)境和參數(shù)
- 吞掉可能出現(xiàn)的錯(cuò)誤和異常
Promise 的特性就是專門用來為這些問題提供一個(gè)有效的可復(fù)用的答案鄙皇。
3.3.1 調(diào)用過早
根據(jù)定義,Promise就不必?fù)?dān)心這種問題癞志,因?yàn)榧词故橇⒓赐瓿傻腜romise(類似于 new Promise(function(resolve){ resolve(42); }) )也無法被同步觀察到唁奢。
也就是說雾叭,對(duì)一個(gè)Promise調(diào)用then()的時(shí)候夷陋,即使這個(gè)Promise已經(jīng)決議,提供給then()的回調(diào)也總會(huì)被異步調(diào)用汤纸。
3.3.2 調(diào)用過晚
Promise創(chuàng)建對(duì)象調(diào)用resolve()或reject()時(shí)衩茸,這個(gè)Promise的then()注冊(cè)的觀察回調(diào)就會(huì)被自動(dòng)調(diào)度≈ⅲ可確信楞慈,這些被調(diào)度的回調(diào)在下一個(gè)異步事件點(diǎn)上一定會(huì)被觸發(fā)。
同步查看是不可能的啃擦,所以一個(gè)同步任務(wù)鏈無法以這種方式運(yùn)行來實(shí)現(xiàn)按照預(yù)期有效延遲另一個(gè)回調(diào)的發(fā)生囊蓝。也就是說,一個(gè)Promise決議后令蛉,這個(gè)Promise上所有的通過then()注冊(cè)的回調(diào)都會(huì)在下一個(gè)異步時(shí)機(jī)點(diǎn)上依次被立即調(diào)用聚霜。這些回調(diào)中的任意一個(gè)都無法影響或延誤對(duì)其他回調(diào)的調(diào)用。
p.then( function(){
p.then( function(){
console.log( "C" );
});
console.log( "A" );
} );
p.then( function(){
console.log( "B" );
});
// A B C
這里珠叔,“C” 無法打斷或搶占“B”蝎宇,這是因?yàn)镻romise的運(yùn)作方式。
Promise 調(diào)度技巧
有很多調(diào)度的細(xì)微差別运杭。這種情況下夫啊,兩個(gè)獨(dú)立Promise上鏈接的回調(diào)的相對(duì)順序無法可靠預(yù)測(cè)函卒。
如果兩個(gè)Promise p1 和 p2都已經(jīng)決議辆憔,那么p1.then(), p2.then()應(yīng)該最終會(huì)制調(diào)用p1的回調(diào)报嵌,然后是p2虱咧。但還有一些微妙的場(chǎng)景可能不是這樣。
var p3 = new Promise( function(resolve, reject){
resolve( "B" );
});
var p1 = new Promise(function(resolve, reject){
resolve( p3 );
})
p2 = new Promise(function(resolve, reject){
resolve( "A" );
})
p1.then( function(v){
console.log( v );
})
p2.then( function(v){
console.log( v );
})
// A B , 而不是像你認(rèn)為的 B A
p1不是用立即值而是用另一個(gè)promise p3決議锚国,后者本身決議為值“B”腕巡。規(guī)定的行為是把p3展開到p1,但是是異步地展開。所以血筑,在異步任務(wù)隊(duì)列中绘沉,p1的回調(diào)排在p2的回調(diào)之后煎楣。
要避免這樣的細(xì)微區(qū)別帶來的噩夢(mèng),你永遠(yuǎn)都不應(yīng)該依賴于不同Promise間回調(diào)的順序和調(diào)度车伞。實(shí)際上择懂,好的編碼實(shí)踐方案根本不會(huì)讓多個(gè)回調(diào)的順序有絲毫影響,可能的話就要避免另玖。
3.3.3 回調(diào)未調(diào)用
首先困曙,沒有任何東西(甚至JS錯(cuò)誤)能阻止Prmise向你通知它的決議(如果它決議了的話)。如果你對(duì)一個(gè)Promise注冊(cè)了一個(gè)完成回調(diào)和一個(gè)拒絕回調(diào)谦去,那么Promise在決議時(shí)總是會(huì)調(diào)用其中一個(gè)慷丽。
當(dāng)然,如果你的回調(diào)函數(shù)本身包含JS錯(cuò)誤鳄哭,那可能就會(huì)看不到你期望的結(jié)果要糊。但實(shí)際上回調(diào)還是被調(diào)用了。后面討論窃诉,這些錯(cuò)誤并不會(huì)被吞掉杨耙。
但是,如果Promise永遠(yuǎn)不決議呢飘痛?即使這樣珊膜,Promise也提供了解決方案。其使用了一種稱為竟態(tài)的高級(jí)抽象機(jī)制:
// 用于超時(shí)一個(gè)Promise的工具
function timeoutPromise(delay){
return new Promise( function(resolve, reject){
setTimeout(function(){
reject("Timeout!");
}, delay)
})
}
// 設(shè)置foo()超時(shí)
Promise.race( [
foo(),
timeoutPromise( 3000 );
])
.then(
function(){
// foo() 及時(shí)完成宣脉!
},
function(err){
// 或者foo()被拒絕车柠,或者只是沒能按時(shí)完成
// 查看err來了解是哪種情況
}
)
我們可保證一個(gè)foo()有一個(gè)信號(hào),防止其永久掛住程序塑猖。
3.3.4 調(diào)用次數(shù)過少或過多
根據(jù)定義竹祷,回調(diào)被調(diào)用的正確次數(shù)應(yīng)該是1⊙蚬叮“過少”的情況就是調(diào)用0次塑陵,和前面解釋過的“未被”調(diào)用是同一種情況。
“過多”容易解釋蜡励。Promise的定義方式使得它只能被決議一次令花。如果出于某種原因,Promise創(chuàng)建代碼試圖調(diào)用resolve()或reject()多次凉倚,或者試圖兩者都調(diào)用兼都,那么這個(gè)Promise將只會(huì)接受第一次決議,并默默地忽略任何后續(xù)調(diào)用稽寒。
由于Promise只能被決議一次扮碧,所以任何通過then()注冊(cè)的(每個(gè))回調(diào)就只會(huì)被調(diào)用一次。
當(dāng)然,如果你把同一個(gè)回調(diào)注冊(cè)了不止一次(比如p.then(f); p.then(f))慎王,那頭被調(diào)用的次數(shù)就會(huì)和注冊(cè)次數(shù)相同蚓土。響應(yīng)函數(shù)只會(huì)被調(diào)用一次。
3.3.5 未能傳遞參數(shù)/環(huán)境值
Promise 至多只能有一個(gè)決議值(完成或拒絕)赖淤。
如果你沒有用任何值顯式?jīng)Q議北戏,那么這個(gè)值就是undefined,這是JS常見的處理方式漫蛔。但不管這個(gè)值是什么嗜愈,無論當(dāng)前或未來,它都會(huì)被傳給所有注冊(cè)的(且適當(dāng)?shù)耐瓿苫蚓芙^)回調(diào)莽龟。
還有一點(diǎn)需要清楚:如果使用多個(gè)參數(shù)調(diào)用resovel()或者reject()第一個(gè)參數(shù)之后的所有參數(shù)都會(huì)被默默忽略蠕嫁。
如果要傳遞多個(gè)值,你就必須要把它們封裝在一個(gè)數(shù)組或?qū)ο笾小?/p>
對(duì)環(huán)境來說毯盈,JS中的函數(shù)總是保持其定義所在的作用域的閉包剃毒,所以它們當(dāng)然可繼續(xù)你提供的環(huán)境狀態(tài)。
3.3.6 吞掉錯(cuò)誤或異常
如果在Promise的創(chuàng)建過程中或在查看其決議結(jié)果過程中的任何時(shí)間點(diǎn)上出現(xiàn)了一個(gè)JS異常錯(cuò)誤搂赋,比如一個(gè)TypeError或RefernceError赘阀,那這個(gè)異常就會(huì)被捕捉,并且會(huì)使這個(gè)Promise被拒絕脑奠。
var p = new Promise( function(resolve, reject){
foo.bar(); // foo 未定義基公,所以會(huì)出錯(cuò)
resolve(42); // 永遠(yuǎn)不會(huì)到達(dá)這里
});
p.then(
function fulfilled(){
// 永遠(yuǎn)不會(huì)到這里
},
function rejected(err){
// err 將會(huì)是一個(gè)TypeError異常對(duì)象來自foo.bar()這一行
}
)
foo.bar()中發(fā)生的JS異常導(dǎo)致了Promise拒絕,你可捕捉并對(duì)其做出響應(yīng)宋欺。
Promise甚至把JS異常也變成了異步行為轰豆,進(jìn)而極大降低了竟態(tài)條件出現(xiàn)的可能。
但是齿诞,如果Promise完成后在查看結(jié)果時(shí)(then()注冊(cè)回調(diào)中)出現(xiàn)了JS異常錯(cuò)誤會(huì)怎樣呢酸休?
var p = new Promise( function(resolve, reject){
resolve( 42 );
});
p.then(
function fulfilled(msg){
foo.bar();
console.log( msg ); // 永遠(yuǎn)不會(huì)到達(dá)這里
},
function rejected(err){
// 永遠(yuǎn)也不會(huì)到達(dá)這里
}
)
等一下,這看qvnn來像是foo.bar()產(chǎn)生的異常真的被吞掉了祷杈。別擔(dān)心斑司,實(shí)際上并不是這樣。但是這里有一個(gè)深的問題但汞。就是我們沒有偵聽到它宿刮。p.then()調(diào)用本身返回了另一個(gè)promise,正是這個(gè)promise將會(huì)因TypeError異常而被拒絕特占。
3.3.7 是可信任的 Promise 嗎
你肯定已經(jīng)注意到Promise并沒有完全擺脫回調(diào)糙置。它們只是改變了傳遞回調(diào)的位置云茸。我們并不是把回調(diào)傳遞給foo()是目,而是從foo()得到某個(gè)東西,然后把回調(diào)傳給這個(gè)東西标捺。
但是懊纳,為什么這就比單純使用回調(diào)更值得信任呢揉抵?
關(guān)于Promise的很重要但是常常被忽略的一個(gè)細(xì)節(jié)是,Promise對(duì)這個(gè)問題已經(jīng)有一個(gè)解決方案嗤疯。包含在原生ES6 Promise實(shí)現(xiàn)中的解決方案就是Promise.resolve()冤今。
如果向Promise.resolve()傳遞一個(gè)非Promise、非thenable的立即值茂缚,就會(huì)得到一個(gè)用這個(gè)值填充的promise戏罢。下面這種情況下,promise p1 和 promise p2 的行為是完全一樣的:
var p1 = new Promise( function(resolve, reject){
resolve( 42 );
} )
var p2 = Promise.resolve(42);
而如果向Promise.resolve() 傳遞一個(gè)真正的Promise脚囊,就只會(huì)返回同一個(gè)promise
var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( p1 );
p1 === p2; // true
如果向Promise.resolve()傳遞了一個(gè)非Promise的thenable 值龟糕,前者會(huì)試圖展開這個(gè)值,而且展開過程會(huì)持續(xù)到提取出一個(gè)具體的非類Promise的最終值悔耘。
var p = {
then: function(cb){
cb( 42 );
}
};
// 這可以工作讲岁,但只是因?yàn)樾疫\(yùn)而已
p
.then(
function fulfilled(val){
console.log( val ); //42
},
function rejected(err){
// 永遠(yuǎn)不會(huì)到這里
}
)
但是,下面又會(huì)怎樣呢衬以?
var p = {
then: function(cb, errcb){
cb(42);
errcb("evil laugh");
}
};
p
.then(
function fulfilled(val){
console.log( val ); //42
},
function rejected(err){
// 啊缓艳,不應(yīng)該運(yùn)行!
console.log( err ); // 邪惡的笑
}
)
盡管如此看峻,我們還是都可把這些版本的p 傳給Promise.resolve()阶淘,然后就會(huì)得到期望中的規(guī)范化后的安全結(jié)果:
Promise.resolve(p)
.then(
function fulfilled(val){
console.log(val); //42
},
function rejected(err){
// 永遠(yuǎn)不會(huì)到這里
}
)
Promise.resolve()可接受任何thenable,將其解封完它的非thenable值互妓。從Promise.resolve()得到的是一個(gè)真正的Promise,是一個(gè)可信任的值舶治。如果你傳入的已經(jīng)是真正的Promise,那們你得到的就是它本身车猬,所以通過Promise.resolve()過濾來獲得可信任性完全沒有壞處霉猛。
假設(shè)我們要調(diào)用一個(gè)工具foo(),且不確定得到的返回值是否是一個(gè)可信任的行為良好的Promise珠闰,但我們可知道它至少是一個(gè)thenable惜浅。Promise.resolve()提供了可信任的Promise封裝工具,可鏈接使用:
// 不要這么做
foo(42)
.then(function(v) {
console.log( v );
});
// 而要這么做
Promise.resolve( foo(42) )
.then( function(v){
console.log(v)
})
對(duì)于用Promise.resolve() 為所有函數(shù)的返回值都封裝一層伏嗜。另一個(gè)好處是坛悉,這樣做很容易把函數(shù)調(diào)用規(guī)范為定義良好的異步任務(wù)。如果foo(42)有時(shí)會(huì)返回一個(gè)立即值承绸,有時(shí)會(huì)返回Promise裸影,那么Promise.resolve(foo(42))就能保證總返回一個(gè)Promise結(jié)果。