感謝社區(qū)中各位的大力支持,譯者再次奉上一點點福利:阿里云產(chǎn)品券辕录,享受所有官網(wǎng)優(yōu)惠睦霎,并抽取幸運大獎:點擊這里領(lǐng)取
錯誤處理
我們已經(jīng)看過幾個例子,Promise拒絕——既可以通過有意調(diào)用reject(..)
走诞,也可以通過意外的JS異掣迸——是如何在異步編程中允許清晰的錯誤處理的。讓我們兜個圈子回去蚣旱,將我們一帶而過的一些細(xì)節(jié)弄清楚碑幅。
對大多數(shù)開發(fā)者來說,最自然的錯誤處理形式是同步的try..catch
結(jié)構(gòu)姻锁。不幸的是枕赵,它僅能用于同步狀態(tài),所以在異步代碼模式中它幫不上什么忙:
function foo() {
setTimeout( function(){
baz.bar();
}, 100 );
}
try {
foo();
// 稍后會從`baz.bar()`拋出全局錯誤
}
catch (err) {
// 永遠(yuǎn)不會到這里
}
能有try..catch
當(dāng)然很好位隶,但除非有某些附加的環(huán)境支持,它無法與異步操作一起工作开皿。我們將會在第四章中討論generator時回到這個話題涧黄。
在回調(diào)中篮昧,對于錯誤處理的模式已經(jīng)有了一些新興的模式,最有名的就是“錯誤優(yōu)先回調(diào)”風(fēng)格:
function foo(cb) {
setTimeout( function(){
try {
var x = baz.bar();
cb( null, x ); // 成功笋妥!
}
catch (err) {
cb( err );
}
}, 100 );
}
foo( function(err,val){
if (err) {
console.error( err ); // 倒霉 :(
}
else {
console.log( val );
}
} );
注意: 這里的try..catch
僅在baz.bar()
調(diào)用立即地懊昨,同步地成功或失敗時才能工作。如果baz.bar()
本身是一個異步完成的函數(shù)春宣,它內(nèi)部的任何異步錯誤都不能被捕獲酵颁。
我們傳遞給foo(..)
的回調(diào)期望通過預(yù)留的err
參數(shù)收到一個表示錯誤的信號。如果存在月帝,就假定出錯躏惋。如果不存在,就假定成功嚷辅。
這類錯誤處理在技術(shù)上是 異步兼容的簿姨,但它根本組織的不好。用無處不在的if
語句檢查將多層錯誤優(yōu)先回調(diào)編織在一起簸搞,將不可避免地將你置于回調(diào)地獄的危險之中(見第二章)扁位。
那么我們回到Promise的錯誤處理,使用傳遞給then(..)
的拒絕處理器趁俊。Promise不使用流行的“錯誤優(yōu)先回調(diào)”設(shè)計風(fēng)格域仇,反而使用“分割回調(diào)”的風(fēng)格;一個回調(diào)給完成寺擂,一個回調(diào)給拒絕:
var p = Promise.reject( "Oops" );
p.then(
function fulfilled(){
// 永遠(yuǎn)不會到這里
},
function rejected(err){
console.log( err ); // "Oops"
}
);
雖然這種模式表面上看起來十分有道理暇务,但是Promise錯誤處理的微妙之處經(jīng)常使它有點兒相當(dāng)難以全面把握。
考慮下面的代碼:
var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// 數(shù)字沒有字符串方法,
// 所以這里拋出一個錯誤
console.log( msg.toLowerCase() );
},
function rejected(err){
// 永遠(yuǎn)不會到這里
}
);
如果msg.toLowerCase()
合法地拋出一個錯誤(它會的9炼铩)般卑,為什么我們的錯誤處理器沒有得到通知?正如我們早先解釋的爽雄,這是因為 這個 錯誤處理器是為p
promise準(zhǔn)備的蝠检,也就是已經(jīng)被值42
完成的那個promise。p
promise是不可變的挚瘟,所以唯一可以得到錯誤通知的promise是由p.then(..)
返回的那個叹谁,而在這里我們沒有捕獲它。
這應(yīng)當(dāng)解釋了:為什么Promise的錯誤處理是易錯的乘盖。錯誤太容易被吞掉了焰檩,而這很少是你有意這么做的。
警告: 如果你以一種不合法的方式使用Promise API订框,而且有錯誤阻止正常的Promise構(gòu)建析苫,其結(jié)果將是一個立即被拋出的異常,而不是一個拒絕Promise。這是一些導(dǎo)致Promise構(gòu)建失敗的錯誤用法:new Promise(null)
衩侥,Promise.all()
国旷,Promise.race(42)
等等。如果你沒有足夠合法地使用Promise API來首先實際構(gòu)建一個Promise茫死,你就不能得到一個拒絕Promise跪但!
絕望的深淵
幾年前Jeff Atwood曾經(jīng)寫到:編程語言總是默認(rèn)地以這樣的方式建立,開發(fā)者們會掉入“絕望的深淵”(http://blog.codinghorror.com/falling-into-the-pit-of-success/ )——在這里意外會被懲罰——而你不得不更努力地使它正確峦萎。他懇求我們相反地創(chuàng)建“成功的深淵”屡久,就是你會默認(rèn)地掉入期望的(成功的)行為,而如此你不得不更努力地去失敗爱榔。
毫無疑問被环,Promise的錯誤處理是一種“絕望的深淵”的設(shè)計。默認(rèn)情況下搓蚪,它假定你想讓所有的錯誤都被Promise的狀態(tài)吞掉蛤售,而且如果你忘記監(jiān)聽這個狀態(tài),錯誤就會默默地凋零/死去——通常是絕望的妒潭。
為了回避把一個被遺忘/拋棄的Promise的錯誤無聲地丟失悴能,一些開發(fā)者宣稱Promise鏈的“最佳實踐”是,總是將你的鏈條以catch(..)
終結(jié)雳灾,就像這樣:
var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// 數(shù)字沒有字符串方法,
// 所以這里拋出一個錯誤
console.log( msg.toLowerCase() );
}
)
.catch( handleErrors );
因為我們沒有給then(..)
傳遞拒絕處理器漠酿,默認(rèn)的處理器會頂替上來,它僅僅簡單地將錯誤傳播到鏈條的下一個promise中谎亩。如此炒嘲,在p
中發(fā)生的錯誤,與在p
之后的解析中(比如msg.toLowerCase()
)發(fā)生的錯誤都將會過濾到最后的handleErrors(..)
中匈庭。
問題解決了夫凸,對吧?沒那么容易阱持!
要是handleErrors(..)
本身也有錯誤呢夭拌?誰來捕獲它?這里還有一個沒人注意的promise:catch(..)
返回的promise衷咽,我們沒有對它進行捕獲鸽扁,也沒注冊拒絕處理器骗随。
你不能僅僅將另一個catch(..)
貼在鏈條末尾蹦玫,因為它也可能失敗弱左。Promise鏈的最后一步损离,無論它是什么,總有可能织中,即便這種可能性逐漸減少苗缩,懸掛著一個困在未被監(jiān)聽的Promise中的崭参,未被捕獲的錯誤。
聽起來像一個不可解的迷吧即横?
處理未被捕獲的錯誤
這不是一個很容易就能完全解決的問題噪生。但是有些接近于解決的方法裆赵,或者說 更好的方法东囚。
一些Promise庫有一些附加的方法,可以注冊某些類似于“全局的未處理拒絕”的處理器战授,全局上不會拋出錯誤页藻,而是調(diào)用它。但是他們識別一個錯誤是“未被捕獲的錯誤”的方案是植兰,使用一個任意長的計時器份帐,比如說3秒,從拒絕的那一刻開始計時楣导。如果一個Promise被拒絕但沒有錯誤處理在計時器被觸發(fā)前注冊废境,那么它就假定你不會注冊監(jiān)聽器了,所以它是“未被捕獲的”筒繁。
實踐中噩凹,這個方法在許多庫中工作的很好,因為大多數(shù)用法不會在Promise拒絕和監(jiān)聽這個拒絕之間有很明顯的延遲毡咏。但是這個模式有點兒麻煩驮宴,因為3秒實在太隨意了(即便它是實證過的),還因為確實有些情況你想讓一個Promise在一段不確定的時間內(nèi)持有它的拒絕狀態(tài)呕缭,而且你不希望你的“未捕獲錯誤”處理器因為這些誤報(還沒處理的“未捕獲錯誤”)而被調(diào)用堵泽。
另一種常見的建議是,Promise應(yīng)當(dāng)增加一個done(..)
方法恢总,它實質(zhì)上標(biāo)志著Promise鏈的“終結(jié)”迎罗。done(..)
不會創(chuàng)建并返回一個Promise,所以傳遞給done(..)
的回調(diào)很明顯地不會鏈接上一個不存在的Promise鏈片仿,并向它報告問題纹安。
那么接下來會發(fā)什么?正如你通常在未處理錯誤狀態(tài)下希望的那樣滋戳,在done(..)
的拒絕處理器內(nèi)部的任何異常都作為全局的未捕獲錯誤拋出(基本上扔到開發(fā)者控制臺):
var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// 數(shù)字沒有字符串方法,
// 所以這里拋出一個錯誤
console.log( msg.toLowerCase() );
}
)
.done( null, handleErrors );
// 如果`handleErrors(..)`自身發(fā)生異常钻蔑,它會在這里被拋出到全局
這聽起來要比永不終結(jié)的鏈條或隨意的超時要吸引人。但最大的問題是奸鸯,它不是ES6標(biāo)準(zhǔn)咪笑,所以不管聽起來多么好,它成為一個可靠而普遍的解決方案還有很長的距離娄涩。
那我們就卡在這里了窗怒?不完全是映跟。
瀏覽器有一個我們的代碼沒有的能力:它們可以追蹤并確定一個對象什么時候被廢棄并可以作為垃圾回收。所以扬虚,瀏覽器可以追蹤Promise對象努隙,當(dāng)它們被當(dāng)做垃圾回收時,如果在它們內(nèi)部存在一個拒絕狀態(tài)辜昵,瀏覽器就可以確信這是一個合法的“未捕獲錯誤”荸镊,它可以信心十足地知道應(yīng)當(dāng)在開發(fā)者控制臺上報告這一情況。
注意: 在寫作本書的時候堪置,Chrome和Firefox都早已試圖實現(xiàn)這種“未捕獲拒絕”的能力躬存,雖然至多也就是支持的不完整。
然而舀锨,如果一個Promise不被垃圾回收——通過許多不同的代碼模式岭洲,這極其容易不經(jīng)意地發(fā)生——瀏覽器的垃圾回收檢測不會幫你知道或診斷你有一個拒絕的Promise靜靜地躺在附近。
還有其他選項嗎坎匿?有盾剩。
成功的深淵
以下講的僅僅是理論上,Promise 可能 在某一天變成什么樣的行為替蔬。我相信那會比我們現(xiàn)在擁有的優(yōu)越許多告私。而且我想這種改變可能會發(fā)生在后ES6時代,因為我不認(rèn)為它會破壞Web的兼容性进栽。另外德挣,如果你小心行事,它是可以被填補(polyfilled)/預(yù)填補(prollyfilled)的快毛。讓我們來看一下:
- Promise可以默認(rèn)為是報告(向開發(fā)者控制臺)一切拒絕的格嗅,就在下一個Job或事件輪詢tick,如果就在這時Promise上沒有注冊任何錯誤處理器唠帝。
- 如果你希望拒絕的Promise在被監(jiān)聽前屯掖,將其拒絕狀態(tài)保持一段不確定的時間。你可以調(diào)用
defer()
襟衰,它會壓制這個Promise自動報告錯誤贴铜。
如果一個Promise被拒絕,默認(rèn)地它會吵吵鬧鬧地向開發(fā)者控制臺報告這個情況(而不是默認(rèn)不出聲)瀑晒。你既可以選擇隱式地處理這個報告(通過在拒絕之前注冊錯誤處理器)绍坝,也可以選擇明確地處理這個報告(使用defer()
)。無論哪種情況苔悦,你 都控制著這種誤報轩褐。
考慮下面的代碼:
var p = Promise.reject( "Oops" ).defer();
// `foo(..)`返回Promise
foo( 42 )
.then(
function fulfilled(){
return p;
},
function rejected(err){
// 處理`foo(..)`的錯誤
}
);
...
我們創(chuàng)建了p
,我們知道我們會為了使用/監(jiān)聽它的拒絕而等待一會兒玖详,所以我們調(diào)用defer()
——如此就不會有全局的報告把介。defer()
單純地返回同一個promise勤讽,為了鏈接的目的。
從foo(..)
返回的promise 當(dāng)即 就添附了一個錯誤處理器拗踢,所以這隱含地跳出了默認(rèn)行為脚牍,而且不會有全局的關(guān)于錯誤的報告。
但是從then(..)
調(diào)用返回的promise沒有defer()
或添附錯誤處理器巢墅,所以如果它被拒絕(從它內(nèi)部的任意一個解析處理器中)诸狭,那么它就會向開發(fā)者控制臺報告一個未捕獲錯誤。
這種設(shè)計稱為成功的深淵砂缩。默認(rèn)情況下作谚,所有的錯誤不是被處理就是被報告——這幾乎是所有開發(fā)者在幾乎所有情況下所期望的。你要么不得不注冊一個監(jiān)聽器庵芭,要么不得不有意什么都不做,并指示你要將錯誤處理推遲到 稍后雀监;你僅為這種特定情況選擇承擔(dān)額外的責(zé)任双吆。
這種方式唯一真正的危險是,你defer()
了一個Promise但是實際上沒有監(jiān)聽/處理它的拒絕会前。
但你不得不有意地調(diào)用defer()
來選擇進入絕望深淵——默認(rèn)是成功深淵——所以對于從你自己的錯誤中拯救你這件事來說好乐,我們能做的不多。
我覺得對于Promise的錯誤處理還有希望(在后ES6時代)瓦宜。我希望上層人物將會重新思考這種情況并考慮選用這種方式蔚万。同時,你可以自己實現(xiàn)這種方式(給讀者們的挑戰(zhàn)練習(xí)A俦印)反璃,或使用一個 聰明 的Promise庫來為你這么做。
注意: 這種錯誤處理/報告的確切的模型已經(jīng)在我的 asynquence Promise抽象庫中實現(xiàn)假夺,我們會在本書的附錄A中討論它淮蜈。
Promise模式
我們已經(jīng)隱含地看到了使用Promise鏈的順序模式(這個-然后-這個-然后-那個的流程控制),但是我們還可以在Promise的基礎(chǔ)上抽象出許多其他種類的異步模式已卷。這些模式用于簡化異步流程控制的的表達——它可以使我們的代碼更易于推理并且更易于維護——即便是我們程序中最復(fù)雜的部分梧田。
有兩個這樣的模式被直接編碼在ES6原生的Promise
實現(xiàn)中,所以我們免費的得到了它們侧蘸,來作為我們其他模式的構(gòu)建塊兒裁眯。
Promise.all([ .. ])
在一個異步序列(Promise鏈)中,在任何給定的時刻都只有一個異步任務(wù)在被協(xié)調(diào)——第2步嚴(yán)格地接著第1步讳癌,而第3步嚴(yán)格地接著第2步穿稳。但要是并發(fā)(也叫“并行地”)地去做兩個或以上的步驟呢?
用經(jīng)典的編程術(shù)語析桥,一個“門(gate)”是一種等待兩個或更多并行/并發(fā)任務(wù)都執(zhí)行完再繼續(xù)的機制司草。它們完成的順序無關(guān)緊要艰垂,只是它們不得不都完成才能讓門打開,繼而讓流程控制通過埋虹。
在Promise API中猜憎,我們稱這種模式為all([ .. ])
。
比方說你想同時發(fā)起兩個Ajax請求搔课,在發(fā)起第三個Ajax請求發(fā)起之前胰柑,等待它們都完成,而不管它們的順序爬泥〖硖郑考慮這段代碼:
// `request(..)`是一個兼容Promise的Ajax工具
// 就像我們在本章早前定義的
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.all( [p1,p2] )
.then( function(msgs){
// `p1`和`p2`都已完成,這里將它們的消息傳入
return request(
"http://some.url.3/?v=" + msgs.join(",")
);
} )
.then( function(msg){
console.log( msg );
} );
Promise.all([ .. ])
期待一個單獨的參數(shù)袍啡,一個array
踩官,一般由Promise的實例組成。從Promise.all([ .. ])
返回的promise將會收到完成的消息(在這段代碼中是msgs
)境输,它是一個由所有被傳入的promise的完成消息按照被傳入的順序構(gòu)成的array
(與完成的順序無關(guān))蔗牡。
注意: 技術(shù)上講,被傳入Promise.all([ .. ])
的array
的值可以包括Promise嗅剖,thenable辩越,甚至是立即值。這個列表中的每一個值都實質(zhì)上通過Promise.resolve(..)
來確保它是一個可以被等待的純粹的Promise信粮,所以一個立即值將被范化為這個值的一個Promise。如果這個array
是空的强缘,主Promise將會立即完成督惰。
從Promise.resolve(..)
返回的主Promise將會在所有組成它的promise完成之后才會被完成。如果其中任意一個promise被拒絕欺旧,Promise.all([ .. ])
的主Promise將立即被拒絕姑丑,并放棄所有其他promise的結(jié)果。
要記得總是給每個promise添加拒絕/錯誤處理器辞友,即使和特別是那個從Promise.all([ .. ])
返回的promise栅哀。
Promise.race([ .. ])
雖然Promise.all([ .. ])
并發(fā)地協(xié)調(diào)多個Promise并假定它們都需要被完成,但是有時候你只想應(yīng)答“沖過終點的第一個Promise”称龙,而讓其他的Promise被丟棄留拾。
這種模式經(jīng)典地被稱為“閂”,但在Promise中它被稱為一個“競合(race)”鲫尊。
警告: 雖然“只有第一個沖過終點的算贏”是一個非常合適被比喻痴柔,但不幸的是“競合(race)”是一個被占用的詞,因為“競合狀態(tài)(race conditions)”通常被認(rèn)為是程序中的Bug(見第一章)疫向。不要把Promise.race([ .. ])
與“競合狀態(tài)(race conditions)”搞混了咳蔚。
“競合狀態(tài)(race conditions)”也期待一個單獨的array
參數(shù)豪嚎,含有一個或多個Promise,thenable谈火,或立即值侈询。與立即值進行競合并沒有多大實際意義,因為很明顯列表中的第一個會勝出——就像賽跑時有一個選手在終點線上起跑糯耍!
和Promise.all([ .. ])
相似扔字,Promise.race([ .. ])
將會在任意一個Promise解析為完成時完成,而且它會在任意一個Promise解析為拒絕時拒絕温技。
注意: 一個“競合(race)”需要至少一個“選手”革为,所以如果你傳入一個空的array
,race([..])
的主Promise將不會立即解析舵鳞,反而是永遠(yuǎn)不會被解析震檩。這是砸自己的腳!ES6應(yīng)當(dāng)將它規(guī)范為要么完成系任,要么拒絕恳蹲,或者要么拋出某種同步錯誤。不幸的是俩滥,因為在ES6的Promise
之前的Promise庫的優(yōu)先權(quán)高,他們不得不把這個坑留在這兒贺奠,所以要小心絕不要傳入一個空array
霜旧。
讓我們重溫剛才的并發(fā)Ajax的例子,但是在p1
和p2
競合的環(huán)境下:
// `request(..)`是一個兼容Promise的Ajax工具
// 就像我們在本章早前定義的
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.race( [p1,p2] )
.then( function(msg){
// `p1`或`p2`會贏得競合
return request(
"http://some.url.3/?v=" + msg
);
} )
.then( function(msg){
console.log( msg );
} );
因為只有一個Promise會勝出儡率,所以完成的值是一個單獨的消息挂据,而不是一個像Promise.all([ .. ])
中那樣的array
。
超時競合
我們早先看過這個例子儿普,描述Promise.race([ .. ])
如何能夠用于表達“promise超時”模式:
// `foo()`是一個兼容Promise
// `timeoutPromise(..)`在早前定義過崎逃,
// 返回一個在指定延遲之后會被拒絕的Promise
// 為`foo()`設(shè)置一個超時
Promise.race( [
foo(), // 嘗試`foo()`
timeoutPromise( 3000 ) // 給它3秒鐘
] )
.then(
function(){
// `foo(..)`及時地完成了!
},
function(err){
// `foo()`要么是被拒絕了眉孩,要么就是沒有及時完成
// 可以考察`err`來知道是哪一個原因
}
);
這種超時模式在絕大多數(shù)情況下工作的很好个绍。但這里有一些微妙的細(xì)節(jié)要考慮,而且坦率的說它們對于Promise.race([ .. ])
和Promise.all([ .. ])
都同樣需要考慮浪汪。
"Finally"
要問的關(guān)鍵問題是巴柿,“那些被丟棄/忽略的promise發(fā)生了什么?”我們不是從性能的角度在問這個問題——它們通常最終會變成垃圾回收的合法對象——而是從行為的角度(副作用等等)死遭。Promise不能被取消——而且不應(yīng)當(dāng)被取消广恢,因為那會摧毀本章稍后的“Promise不可取消”一節(jié)中要討論的外部不可變性——所以它們只能被無聲地忽略。
但如果前面例子中的foo()
占用了某些資源呀潭,但超時首先觸發(fā)而且導(dǎo)致這個promise被忽略了呢钉迷?這種模式中存在某種東西可以在超時后主動釋放被占用的資源至非,或者取消任何它可能帶來的副作用嗎?要是你想做的全部只是記錄下foo()
超時的事實呢糠聪?
一些開發(fā)者提議荒椭,Promise需要一個finally(..)
回調(diào)注冊機制,它總是在Promise解析時被調(diào)用枷颊,而且允許你制定任何可能的清理操作戳杀。在當(dāng)前的語言規(guī)范中它還不存在,但它可能會在ES7+中加入夭苗。我們不得不邊走邊看了信卡。
它看起來可能是這樣:
var p = Promise.resolve( 42 );
p.then( something )
.finally( cleanup )
.then( another )
.finally( cleanup );
注意: 在各種Promise庫中,finally(..)
依然會創(chuàng)建并返回一個新的Promise(為了使鏈條延續(xù)下去)题造。如果cleanup(..)
函數(shù)返回一個Promise傍菇,它將會鏈入鏈條,這意味著你可能還有我們剛才討論的未處理拒絕的問題界赔。
同時丢习,我們可以制造一個靜態(tài)的幫助工具來讓我們觀察(但不干涉)Promise的解析:
// 填補的安全檢查
if (!Promise.observe) {
Promise.observe = function(pr,cb) {
// 從側(cè)面觀察`pr`的解析
pr.then(
function fulfilled(msg){
// 異步安排回調(diào)(作為Job)
Promise.resolve( msg ).then( cb );
},
function rejected(err){
// 異步安排回調(diào)(作為Job)
Promise.resolve( err ).then( cb );
}
);
// 返回原本的promise
return pr;
};
}
這是我們在前面的超時例子中如何使用它:
Promise.race( [
Promise.observe(
foo(), // 嘗試`foo()`
function cleanup(msg){
// 在`foo()`之后進行清理,即便它沒有及時完成
}
),
timeoutPromise( 3000 ) // 給它3秒鐘
] )
這個Promise.observe(..)
幫助工具只是描述你如何在不干擾Promise的情況下觀測它的完成淮悼。其他的Promise庫有他們自己的解決方案咐低。不論你怎么做,你都將很可能有個地方想用來確認(rèn)你的Promise沒有意外地被無聲地忽略掉袜腥。
all([ .. ]) 與 race([ .. ]) 的變種
原生的ES6Promise帶有內(nèi)建的Promise.all([ .. ])
和Promise.race([ .. ])
见擦,這里還有幾個關(guān)于這些語義的其他常用的變種模式:
-
none([ .. ])
很像all([ .. ])
,但是完成和拒絕被轉(zhuǎn)置了羹令。所有的Promise都需要被拒絕——拒絕變成了完成值鲤屡,反之亦然。 -
any([ .. ])
很像all([ .. ])
福侈,但它忽略任何拒絕酒来,所以只有一個需要完成即可,而不是它們所有的肪凛。 -
first([ .. ])
像是一個帶有any([ .. ])
的競合堰汉,它忽略任何拒絕,而且一旦有一個Promise完成時显拜,它就立即完成衡奥。 -
last([ .. ])
很像first([ .. ])
,但是只有最后一個完成勝出远荠。
某些Promise抽象工具庫提供這些方法矮固,但你也可以用Promise機制的race([ .. ])
和all([ .. ])
,自己定義他們。
比如档址,這是我們?nèi)绾味xfirst([..])
:
// 填補的安全檢查
if (!Promise.first) {
Promise.first = function(prs) {
return new Promise( function(resolve,reject){
// 迭代所有的promise
prs.forEach( function(pr){
// 泛化它的值
Promise.resolve( pr )
// 無論哪一個首先成功完成盹兢,都由它來解析主promise
.then( resolve );
} );
} );
};
}
注意: 這個first(..)
的實現(xiàn)不會在它所有的promise都被拒絕時拒絕;它會簡單地掛起守伸,很像Promise.race([])
绎秒。如果需要,你可以添加一些附加邏輯來追蹤每個promise的拒絕尼摹,而且如果所有的都被拒絕见芹,就在主promise上調(diào)用reject()
。我們將此作為練習(xí)留給讀者蠢涝。
并發(fā)迭代
有時候你想迭代一個Promise的列表玄呛,并對它們所有都實施一些任務(wù),就像你可以對同步的array
做的那樣(比如和二,forEach(..)
徘铝,map(..)
,some(..)
惯吕,和every(..)
)惕它。如果對每個Promise實施的操作根本上是同步的,它們工作的很好废登,正如我們在前面的代碼段中用過的forEach(..)
淹魄。
但如果任務(wù)在根本上是異步的,或者可以/應(yīng)當(dāng)并發(fā)地實施堡距,你可以使用許多庫提供的異步版本的這些工具方法揭北。
比如,讓我們考慮一個異步的map(..)
工具吏颖,它接收一個array
值(可以是Promise或任何東西),外加一個對數(shù)組中每一個值實施的函數(shù)(任務(wù))恨樟。map(..)
本身返回一個promise半醉,它的完成值是一個持有每個任務(wù)的異步完成值的array
(以與映射(mapping)相同的順序):
if (!Promise.map) {
Promise.map = function(vals,cb) {
// 一個等待所有被映射的promise的新promise
return Promise.all(
// 注意:普通的數(shù)組`map(..)`,
// 將值的數(shù)組變?yōu)閜romise的數(shù)組
vals.map( function(val){
// 將`val`替換為一個在`val`
// 異步映射完成后才解析的新promise
return new Promise( function(resolve){
cb( val, resolve );
} );
} )
);
};
}
注意: 在這種map(..)
的實現(xiàn)中劝术,你無法表示異步拒絕缩多,但如果一個在映射的回調(diào)內(nèi)部發(fā)生一個同步的異常/錯誤,那么Promise.map(..)
返回的主Promise就會拒絕养晋。
讓我們描繪一下對一組Promise(不是簡單的值)使用map(..)
:
var p1 = Promise.resolve( 21 );
var p2 = Promise.resolve( 42 );
var p3 = Promise.reject( "Oops" );
// 將列表中的值翻倍衬吆,即便它們在Promise中
Promise.map( [p1,p2,p3], function(pr,done){
// 確保列表中每一個值都是Promise
Promise.resolve( pr )
.then(
// 將值作為`v`抽取出來
function(v){
// 將完成的`v`映射到新的值
done( v * 2 );
},
// 或者,映射到promise的拒絕消息上
done
);
} )
.then( function(vals){
console.log( vals ); // [42,84,"Oops"]
} );
Promise API概覽
讓我們復(fù)習(xí)一下我們已經(jīng)在本章中零散地展開的ES6Promise
API绳泉。
注意: 下面的API盡管在ES6中是原生的,但也存在一些語言規(guī)范兼容的填補(不光是擴展Promise庫),它們定義了Promise
和與之相關(guān)的所有行為甚垦,所以即使是在前ES6時代的瀏覽器中你也以使用原生的Promise。這類填補的其中之一是“Native Promise Only”(http://github.com/getify/native-promise-only)拇勃,我寫的!
new Promise(..)構(gòu)造器
揭示構(gòu)造器(revealing constructor) Promise(..)
必須與new
一起使用孝凌,而且必須提供一個被同步/立即調(diào)用的回調(diào)函數(shù)方咆。這個函數(shù)被傳入兩個回調(diào)函數(shù),它們作為promise的解析能力蟀架。我們通常將它們標(biāo)識為resolve(..)
和reject(..)
:
var p = new Promise( function(resolve,reject){
// `resolve(..)`給解析/完成的promise
// `reject(..)`給拒絕的promise
} );
reject(..)
簡單地拒絕promise瓣赂,但是resolve(..)
既可以完成promise,也可以拒絕promise片拍,這要看它被傳入什么值煌集。如果resolve(..)
被傳入一個立即的,非Promise穆碎,非thenable的值牙勘,那么這個promise將用這個值完成。
但如果resolve(..)
被傳入一個Promise或者thenable的值所禀,那么這個值將被遞歸地展開方面,而且無論它最終解析結(jié)果/狀態(tài)是什么,都將被promise采用色徘。
Promise.resolve(..) 和 Promise.reject(..)
一個用于創(chuàng)建已被拒絕的Promise的簡便方法是Promise.reject(..)
恭金,所以這兩個promise是等價的:
var p1 = new Promise( function(resolve,reject){
reject( "Oops" );
} );
var p2 = Promise.reject( "Oops" );
與Promise.reject(..)
相似,Promise.resolve(..)
通常用來創(chuàng)建一個已完成的Promise褂策。然而横腿,Promise.resolve(..)
還會展開thenale值(就像我們已經(jīng)幾次討論過的)。在這種情況下斤寂,返回的Promise將會采用你傳入的thenable的解析耿焊,它既可能是完成,也可能是拒絕:
var fulfilledTh = {
then: function(cb) { cb( 42 ); }
};
var rejectedTh = {
then: function(cb,errCb) {
errCb( "Oops" );
}
};
var p1 = Promise.resolve( fulfilledTh );
var p2 = Promise.resolve( rejectedTh );
// `p1`將是一個完成的promise
// `p2`將是一個拒絕的promise
而且要記住遍搞,如果你傳入一個純粹的Promise罗侯,Promise.resolve(..)
不會做任何事情;它僅僅會直接返回這個值溪猿。所以在你不知道其本性的值上調(diào)用Promise.resolve(..)
不會有額外的開銷钩杰,如果它偶然已經(jīng)是一個純粹的Promise。
then(..) 和 catch(..)
每個Promise實例(不是 Promise
API 名稱空間)都有then(..)
和catch(..)
方法诊县,它們允許你為Promise注冊成功或拒絕處理器讲弄。一旦Promise被解析,它們中的一個就會被調(diào)用依痊,但不是都會被調(diào)用避除,而且它們總是會被異步地調(diào)用(參見第一章的“Jobs”)。
then(..)
接收兩個參數(shù),第一個用于完成回調(diào)驹饺,第二個用戶拒絕回調(diào)钳枕。如果它們其中之一被省略,或者被傳入一個非函數(shù)的值赏壹,那么一個默認(rèn)的回調(diào)就會分別頂替上來鱼炒。默認(rèn)的完成回調(diào)簡單地將值向下傳遞,而默認(rèn)的拒絕回調(diào)簡單地重新拋出(傳播)收到的拒絕理由蝌借。
catch(..)
僅僅接收一個拒絕回調(diào)作為參數(shù)昔瞧,而且會自動的頂替一個默認(rèn)的成功回調(diào),就像我們討論過的菩佑。換句話說自晰,它等價于then(null,..)
:
p.then( fulfilled );
p.then( fulfilled, rejected );
p.catch( rejected ); // 或者`p.then( null, rejected )`
then(..)
和catch(..)
也會創(chuàng)建并返回一個新的promise,它可以用來表達Promise鏈?zhǔn)搅鞒炭刂粕耘鳌H绻瓿苫蚓芙^回調(diào)有異常被拋出酬荞,這個返回的promise就會被拒絕。如果這兩個回調(diào)之一返回一個立即瞧哟,非Promise混巧,非thenable值,那么這個值就會作為被返回的promise的完成勤揩。如果完成處理器指定地返回一個promise或thenable值這個值就會被展開而且變成被返回的promise的解析咧党。
Promise.all([ .. ]) 和 Promise.race([ .. ])
在ES6的Promise
API的靜態(tài)幫助方法Promise.all([ .. ])
和Promise.race([ .. ])
都創(chuàng)建一個Promise作為它們的返回值。這個promise的解析完全由你傳入的promise數(shù)組控制陨亡。
對于Promise.all([ .. ])
傍衡,為了被返回的promise完成,所有你傳入的promise都必須完成负蠕。如果其中任意一個被拒絕蛙埂,返回的主promise也會立即被拒絕(丟棄其他所有promise的結(jié)果)。至于完成狀態(tài)遮糖,你會收到一個含有所有被傳入的promise的完成值的array
箱残。至于拒絕狀態(tài),你僅會收到第一個promise拒絕的理由值止吁。這種模式通常稱為“門”:在門打開前所有人都必須到達。
對于Promise.race([ .. ])
燎悍,只有第一個解析(成功或拒絕)的promise會“勝出”敬惦,而且不論解析的結(jié)果是什么,都會成為被返回的promise的解析結(jié)果谈山。這種模式通常成為“閂”:第一個打開門閂的人才能進來俄删。考慮這段代碼:
var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( "Hello World" );
var p3 = Promise.reject( "Oops" );
Promise.race( [p1,p2,p3] )
.then( function(msg){
console.log( msg ); // 42
} );
Promise.all( [p1,p2,p3] )
.catch( function(err){
console.error( err ); // "Oops"
} );
Promise.all( [p1,p2] )
.then( function(msgs){
console.log( msgs ); // [42,"Hello World"]
} );
警告: 要小心!如果一個空的array
被傳入Promise.all([ .. ])
畴椰,它會立即完成臊诊,但Promise.race([ .. ])
卻會永遠(yuǎn)掛起,永遠(yuǎn)不會解析斜脂。
ES6的Promise
API十分簡單和直接抓艳。對服務(wù)于大多數(shù)基本的異步情況來說它足夠好了,而且當(dāng)你要把你的代碼從回調(diào)地獄變?yōu)槟承└玫臇|西時帚戳,它是一個開始的好地方玷或。
但是依然還有許多應(yīng)用程序所要求的精巧的異步處理,由于Promise本身所受的限制而不能解決片任。在下一節(jié)中偏友,為了有效利用Promise庫,我們將深入檢視這些限制对供。
Promise限制
本節(jié)中我們將要討論的許多細(xì)節(jié)已經(jīng)在這一章中被提及了位他,但我們將明確地復(fù)習(xí)這些限制。
順序的錯誤處理
我們在本章前面的部分詳細(xì)講解了Promise風(fēng)格的錯誤處理产场。Promise的設(shè)計方式——特別是他們?nèi)绾捂溄印a(chǎn)生的限制鹅髓,創(chuàng)建了一個非常容易掉進去的陷阱,Promise鏈中的錯誤會被意外地?zé)o聲地忽略掉涝动。
但關(guān)于Promise的錯誤還有一些其他事情要考慮迈勋。因為Promise鏈只不過是將組成它的Promise連在一起,沒有一個實體可以用來將整個鏈條表達為一個單獨的 東西醋粟,這意味著沒有外部的方法能夠監(jiān)聽可能發(fā)生的任何錯誤靡菇。
如果你構(gòu)建一個不包含錯誤處理器的Promise鏈,這個鏈條的任意位置發(fā)生的任何錯誤都將沿著鏈條向下無限傳播米愿,直到被監(jiān)聽為止(通過在某一步上注冊拒絕處理器)厦凤。所以,在這種特定情況下育苟,擁有鏈條的最后一個promise的引用就夠了(下面代碼段中的p
)较鼓,因為你可以在這里注冊拒絕處理器,而且它會被所有傳播的錯誤通知:
// `foo(..)`, `STEP2(..)` 和 `STEP3(..)`
// 都是promise兼容的工具
var p = foo( 42 )
.then( STEP2 )
.then( STEP3 );
雖然這看起來有點兒小糊涂违柏,但是這里的p
沒有指向鏈條中的第一個promise(foo(42)
調(diào)用中來的那一個)博烂,而是指向了最后一個promise,來自于then(STEP3)
調(diào)用的那一個漱竖。
另外禽篱,這個promise鏈條上看不到一個步驟做了自己的錯誤處理。這意味著你可以在p
上注冊一個拒絕處理器馍惹,如果在鏈條的任意位置發(fā)生了錯誤躺率,它就會被通知玛界。
p.catch( handleErrors );
但如果這個鏈條中的某一步事實上做了自己的錯誤處理(也許是隱藏/抽象出去了,所以你看不到)悼吱,那么你的handleErrors(..)
就不會被通知慎框。這可能是你想要的——它畢竟是一個“被處理過的拒絕”——但它也可能 不 是你想要的。完全缺乏被通知的能力(被“已處理過的”拒絕錯誤通知)是一個在某些用法中約束功能的一種限制后添。
它基本上和try..catch
中存在的限制是相同的笨枯,它可以捕獲一個異常并簡單地吞掉。所以這不是一個 Promise特有 的問題吕朵,但它確實是一個我們希望繞過的限制猎醇。
不幸的是,許多時候Promise鏈序列的中間步驟不會被留下引用努溃,所以沒有這些引用硫嘶,你就不能添加錯誤處理器來可靠地監(jiān)聽錯誤。
單獨的值
根據(jù)定義梧税,Promise只能有一個單獨的完成值或一個單獨的拒絕理由沦疾。在簡單的例子中,這沒什么大不了的第队,但在更精巧的場景下哮塞,你可能發(fā)現(xiàn)這個限制。
通常的建議是構(gòu)建一個包裝值(比如object
或array
)來包含這些多個消息凳谦。這個方法好用忆畅,但是在你的Promise鏈的每一步上把消息包裝再拆開顯得十分尷尬和煩人。
分割值
有時你可以將這種情況當(dāng)做一個信號尸执,表示你可以/應(yīng)當(dāng)將問題拆分為兩個或更多的Promise家凯。
想象你有一個工具foo(..)
,它異步地產(chǎn)生兩個值(x
和y
):
function getY(x) {
return new Promise( function(resolve,reject){
setTimeout( function(){
resolve( (3 * x) - 1 );
}, 100 );
} );
}
function foo(bar,baz) {
var x = bar * baz;
return getY( x )
.then( function(y){
// 將兩個值包裝近一個容器
return [x,y];
} );
}
foo( 10, 20 )
.then( function(msgs){
var x = msgs[0];
var y = msgs[1];
console.log( x, y ); // 200 599
} );
首先如失,讓我們重新安排一下foo(..)
返回的東西绊诲,以便于我們不必再將x
和y
包裝進一個單獨的array
值中來傳送給一個Promise。相反褪贵,我們將每一個值包裝進它自己的promise:
function foo(bar,baz) {
var x = bar * baz;
// 將兩個promise返回
return [
Promise.resolve( x ),
getY( x )
];
}
Promise.all(
foo( 10, 20 )
)
.then( function(msgs){
var x = msgs[0];
var y = msgs[1];
console.log( x, y );
} );
一個promise的array
真的要比傳遞給一個單獨的Promise的值的array
要好嗎掂之?語法上,它沒有太多改進脆丁。
但是這種方式更加接近于Promise的設(shè)計原理∈澜ⅲ現(xiàn)在它更易于在未來將x
與y
的計算分開,重構(gòu)進兩個分離的函數(shù)中槽卫。它更清晰冯乘,也允許調(diào)用端代碼更靈活地安排這兩個promise——這里使用了Promise.all([ .. ])
,但它當(dāng)然不是唯一的選擇——而不是將這樣的細(xì)節(jié)在foo(..)
內(nèi)部進行抽象晒夹。
展開/散開參數(shù)
var x = ..
和var y = ..
的賦值依然是一個尷尬的負(fù)擔(dān)裆馒。我們可以在一個幫助工具中利用一些函數(shù)式技巧(向Reginald Braithwaite致敬,在推特上 @raganwald ):
function spread(fn) {
return Function.apply.bind( fn, null );
}
Promise.all(
foo( 10, 20 )
)
.then(
spread( function(x,y){
console.log( x, y ); // 200 599
} )
)
看起來好些了丐怯!當(dāng)然喷好,你可以內(nèi)聯(lián)這個函數(shù)式魔法來避免額外的幫助函數(shù):
Promise.all(
foo( 10, 20 )
)
.then( Function.apply.bind(
function(x,y){
console.log( x, y ); // 200 599
},
null
) );
這個技巧可能很整潔,但是ES6給了我們一個更好的答案:解構(gòu)(destructuring)读跷。數(shù)組的解構(gòu)賦值形式看起來像這樣:
Promise.all(
foo( 10, 20 )
)
.then( function(msgs){
var [x,y] = msgs;
console.log( x, y ); // 200 599
} );
最棒的是梗搅,ES6提供了數(shù)組參數(shù)解構(gòu)形式:
Promise.all(
foo( 10, 20 )
)
.then( function([x,y]){
console.log( x, y ); // 200 599
} );
我們現(xiàn)在已經(jīng)接受了“每個Promise一個值”的準(zhǔn)則,繼續(xù)讓我們把模板代碼最小化效览!
注意: 更多關(guān)于ES6解構(gòu)形式的信息无切,參閱本系列的 ES6與未來。
單次解析
Promise的一個最固有的行為之一就是丐枉,一個Promise只能被解析一次(成功或拒絕)哆键。對于多數(shù)異步用例來說,你僅僅取用這個值一次瘦锹,所以這工作的很好籍嘹。
但也有許多異步情況適用于一個不同的模型——更類似于事件和/或數(shù)據(jù)流。表面上看不清Promise能對這種用例適應(yīng)的多好弯院,如果能的話辱士。沒有基于Promise的重大抽象過程,它們完全缺乏對多個值解析的處理听绳。
想象這樣一個場景颂碘,你可能想要為響應(yīng)一個刺激(比如事件)觸發(fā)一系列異步處理步驟,而這實際上將會發(fā)生多次椅挣,比如按鈕點擊头岔。
這可能不會像你想的那樣工作:
// `click(..)` 綁定了一個DOM元素的 `"click"` 事件
// `request(..)` 是先前定義的支持Promise的Ajax
var p = new Promise( function(resolve,reject){
click( "#mybtn", resolve );
} );
p.then( function(evt){
var btnID = evt.currentTarget.id;
return request( "http://some.url.1/?id=" + btnID );
} )
.then( function(text){
console.log( text );
} );
這里的行為僅能在你的應(yīng)用程序只讓按鈕被點擊一次的情況下工作。如果按鈕被點擊第二次贴妻,promisep
已經(jīng)被解析了切油,所以第二個resolve(..)
將被忽略。
相反的名惩,你可能需要將模式反過來澎胡,在每次事件觸發(fā)時創(chuàng)建一個全新的Promise鏈:
click( "#mybtn", function(evt){
var btnID = evt.currentTarget.id;
request( "http://some.url.1/?id=" + btnID )
.then( function(text){
console.log( text );
} );
} );
這種方式會 好用,為每個按鈕上的"click"
事件發(fā)起一個全新的Promise序列娩鹉。
但是除了在事件處理器內(nèi)部定義一整套Promise鏈看起來很丑以外攻谁,這樣的設(shè)計在某種意義上違背了關(guān)注/能力分離原則(SoC)。你可能非常想在一個你的代碼不同的地方定義事件處理器:你定義對事件的 響應(yīng)(Promise鏈)的地方弯予。如果沒有幫助機制戚宦,在這種模式下這么做很尷尬。
注意: 這種限制的另一種表述方法是锈嫩,如果我們能夠構(gòu)建某種能在它上面進行Promise鏈監(jiān)聽的“可監(jiān)聽對象(observable)”就好了受楼。有一些庫已經(jīng)建立這些抽象(比如RxJS——http://rxjs.codeplex.com/)垦搬,但是這種抽象看起來是如此的重,以至于你甚至再也看不到Promise的性質(zhì)艳汽。這樣的重抽象帶來一個重要的問題:這些機制是否像Promise本身被設(shè)計的一樣 可靠猴贰。我們將會在附錄B中重新討論“觀察者(Observable)”模式。
惰性
對于在你的代碼中使用Promise而言一個實在的壁壘是河狐,現(xiàn)存的所有代碼都沒有支持Promise米绕。如果你有許多基于回調(diào)的代碼,讓代碼保持相同的風(fēng)格容易多了馋艺。
“一段基于動作(用回調(diào))的代碼將仍然基于動作(用回調(diào))栅干,除非一個更聰明,具有Promise意識的開發(fā)者對它采取行動捐祠〖盍郏”
Promise提供了一種不同的模式規(guī)范,如此雏赦,代碼的表達方式可能會變得有一點兒不同劫笙,某些情況下,則根本不同星岗。你不得不有意這么做填大,因為Promise不僅只是把那些為你服務(wù)至今的老式編碼方法自然地抖落掉。
考慮一個像這樣的基于回調(diào)的場景:
function foo(x,y,cb) {
ajax(
"http://some.url.1/?x=" + x + "&y=" + y,
cb
);
}
foo( 11, 31, function(err,text) {
if (err) {
console.error( err );
}
else {
console.log( text );
}
} );
將這個基于回調(diào)的代碼轉(zhuǎn)換為支持Promise的代碼的第一步該怎么做俏橘,是立即明確的嗎允华?這要看你的經(jīng)驗。你練習(xí)的越多寥掐,它就感覺越自然靴寂。但當(dāng)然,Promise沒有明確告知到底怎么做——沒有一個放之四海而皆準(zhǔn)的答案——所以這要靠你的責(zé)任心召耘。
就像我們以前講過的百炬,我們絕對需要一種支持Promise的Ajax工具來取代基于回調(diào)的工具,我們可以稱它為request(..)
污它。你可以制造自己的剖踊,正如我們已經(jīng)做過的。但是不得不為每個基于回調(diào)的工具手動定義Promise相關(guān)的包裝器的負(fù)擔(dān)衫贬,使得你根本就不太可能選擇將代碼重構(gòu)為Promise相關(guān)的德澈。
Promise沒有為這種限制提供直接的答案。但是大多數(shù)Promise庫確實提供了幫助函數(shù)固惯。想象一個這樣的幫助函數(shù):
// 填補的安全檢查
if (!Promise.wrap) {
Promise.wrap = function(fn) {
return function() {
var args = [].slice.call( arguments );
return new Promise( function(resolve,reject){
fn.apply(
null,
args.concat( function(err,v){
if (err) {
reject( err );
}
else {
resolve( v );
}
} )
);
} );
};
};
}
好吧梆造,這可不是一個微不足道的工具。然而葬毫,雖然他可能看起來有點兒令人生畏镇辉,但也沒有你想的那么糟屡穗。它接收一個函數(shù),這個函數(shù)期望一個錯誤優(yōu)先風(fēng)格的回調(diào)作為第一個參數(shù)忽肛,然后返回一個可以自動創(chuàng)建Promise并返回的新函數(shù)鸡捐,然后為你替換掉回調(diào),與Promise的完成/拒絕連接在一起麻裁。
與其浪費太多時間談?wù)撨@個Promise.wrap(..)
幫助函數(shù) 如何 工作,還不如讓我們來看看如何使用它:
var request = Promise.wrap( ajax );
request( "http://some.url.1/" )
.then( .. )
..
哇哦源祈,真簡單煎源!
Promise.wrap(..)
不會 生產(chǎn)Promise。它生產(chǎn)一個將會生產(chǎn)Promise的函數(shù)香缺。某種意義上手销,一個Promise生產(chǎn)函數(shù)可以被看做一個“Promise工廠”。我提議將這樣的東西命名為“promisory”("Promise" + "factory")图张。
這種將期望回調(diào)的函數(shù)包裝為一個Promise相關(guān)的函數(shù)的行為锋拖,有時被稱為“提升(lifting)”或“promise化(promisifying)”。但是除了“提升過的函數(shù)”以外祸轮,看起來沒有一個標(biāo)準(zhǔn)的名詞來稱呼這個結(jié)果函數(shù)兽埃,所以我更喜歡“promisory”,因為我認(rèn)為他更具描述性适袜。
注意: Promisory不是一個瞎編的詞柄错。它是一個真實存在的詞匯,而且它的定義是含有或載有一個promise苦酱。這正是這些函數(shù)所做的售貌,所以這個術(shù)語匹配得簡直完美!
那么疫萤,Promise.wrap(ajax)
生產(chǎn)了一個我們稱為request(..)
的ajax(..)
promisory颂跨,而這個promisory為Ajax應(yīng)答生產(chǎn)Promise。
如果所有的函數(shù)已經(jīng)都是promisory扯饶,我們就不需要自己制造它們恒削,所以額外的步驟就有點兒多余。但是至少包裝模式是(通常都是)可重復(fù)的帝际,所以我們可以把它放進Promise.wrap(..)
幫助函數(shù)中來支援我們的promise編碼蔓同。
那么回到剛才的例子,我們需要為ajax(..)
和foo(..)
都做一個promisory蹲诀。
// 為`ajax(..)`制造一個promisory
var request = Promise.wrap( ajax );
// 重構(gòu)`foo(..)`斑粱,但是為了代碼其他部分
// 的兼容性暫且保持它對外是基于回調(diào)的
// ——僅在內(nèi)部使用`request(..)`'的promise
function foo(x,y,cb) {
request(
"http://some.url.1/?x=" + x + "&y=" + y
)
.then(
function fulfilled(text){
cb( null, text );
},
cb
);
}
// 現(xiàn)在,為了這段代碼本來的目的脯爪,為`foo(..)`制造一個promisory
var betterFoo = Promise.wrap( foo );
// 并使用這個promisory
betterFoo( 11, 31 )
.then(
function fulfilled(text){
console.log( text );
},
function rejected(err){
console.error( err );
}
);
當(dāng)然则北,雖然我們將foo(..)
重構(gòu)為使用我們的新request(..)
promisory矿微,我們可以將foo(..)
本身制成promisory,而不是保留基于會掉的實現(xiàn)并需要制造和使用后續(xù)的betterFoo(..)
promisory尚揣。這個決定只是要看foo(..)
是否需要保持基于回調(diào)的形式以便于代碼的其他部分兼容涌矢。
考慮這段代碼:
// 現(xiàn)在,`foo(..)`也是一個promisory
// 因為它委托到`request(..)` promisory
function foo(x,y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
foo( 11, 31 )
.then( .. )
..
雖然ES6的Promise沒有為這樣的promisory包裝提供原生的幫助函數(shù)快骗,但是大多數(shù)庫提供它們娜庇,或者你可以制造自己的。不管哪種方法方篮,這種Promise特定的限制是可以不費太多勁兒就可以解決的(當(dāng)然是和回調(diào)地獄的痛苦相比C恪)。
Promise不可撤銷
一旦你創(chuàng)建了一個Promise并給它注冊了一個完成和/或拒絕處理器藕溅,就沒有什么你可以從外部做的事情能停止這個進程匕得,即使是某些其他的事情使這個任務(wù)變得毫無意義。
注意: 許多Promise抽象庫都提供取消Promise的功能巾表,但這是一個非常壞的主意汁掠!許多開發(fā)者都希望Promise被原生地設(shè)計為具有外部取消能力,但問題是這將允許Promise的一個消費者/監(jiān)聽器影響某些其他消費者監(jiān)聽同一個Promise的能力集币。這違反了未來值得可靠性原則(外部不可變)考阱,另外就是嵌入了“遠(yuǎn)距離行為(action at a distance)”的反模式(http://en.wikipedia.org/wiki/Action_at_a_distance_%28computer_programming%29)。不管它看起來多么有用惠猿,它實際上會直接將你引回與回調(diào)地獄相同的噩夢羔砾。
考慮我們早先的Promise超時場景:
var p = foo( 42 );
Promise.race( [
p,
timeoutPromise( 3000 )
] )
.then(
doSomething,
handleError
);
p.then( function(){
// 即使是在超時的情況下也會發(fā)生 :(
} );
“超時”對于promisep
來說是外部的,所以p
本身繼續(xù)運行偶妖,這可能不是我們想要的姜凄。
一個選項是侵入性地定義你的解析回調(diào):
var OK = true;
var p = foo( 42 );
Promise.race( [
p,
timeoutPromise( 3000 )
.catch( function(err){
OK = false;
throw err;
} )
] )
.then(
doSomething,
handleError
);
p.then( function(){
if (OK) {
// 僅在沒有超時的情況下發(fā)生! :)
}
} );
這很難看趾访。這可以工作态秧,但是遠(yuǎn)不理想。一般來說扼鞋,你應(yīng)當(dāng)避免這樣的場景申鱼。
但是如果你不能,這種解決方案的丑陋應(yīng)當(dāng)是一個線索云头,說明 取消 是一種屬于在Promise之上的更高層抽象的功能捐友。我推薦你找一個Promise抽象庫來輔助你,而不是自己使用黑科技溃槐。
注意: 我的 asynquence Promise抽象庫提供了這樣的抽象匣砖,還為序列提供了一個abort()
能力,這一切將在附錄A中討論。
一個單獨的Promise不是真正的流程控制機制(至少沒有多大實際意義)猴鲫,而流程控制機制正是 取消 要表達的对人;這就是為什么Promise取消顯得尷尬。
相比之下拂共,一個鏈條的Promise集合在一起——我稱之為“序列”—— 是 一個流程控制的表達牺弄,如此在這一層面的抽象上它就適于定義取消成福。
沒有一個單獨的Promise應(yīng)該是可以取消的缀辩,但是一個 序列 可以取消是有道理的,因為你不會將一個序列作為一個不可變值傳來傳去原叮,就像Promise那樣抚恒。
Promise性能
這種限制既簡單又復(fù)雜培慌。
比較一下在基于回調(diào)的異步任務(wù)鏈和Promise鏈上有多少東西在動,很明顯Promise有多得多的事情發(fā)生柑爸,這意味著它們自然地會更慢一點點『幸簦回想一下Promise提供的保證信任的簡單列表表鳍,將它和你為了達到相同保護效果而在回調(diào)上面添加的特殊代碼比較一下。
更多工作要做祥诽,更多的安全要保護譬圣,意味著Promise與赤裸裸的,不可靠的回調(diào)相比 確實 更慢雄坪。這些都很明顯厘熟,可能很容易縈繞在你腦海中。
但是慢多少维哈?好吧……這實際上是一個難到不可思議的問題绳姨,無法絕對,全面地回答阔挠。
坦白地說飘庄,這是一個比較蘋果和橘子的問題,所以可能是問錯了购撼。你實際上應(yīng)當(dāng)比較的是跪削,帶有所有手動保護層的經(jīng)過特殊處理的回調(diào)系統(tǒng),是否比一個Promise實現(xiàn)要快迂求。
如果說Promise有一種合理的性能限制碾盐,那就是它并不將可靠性保護的選項羅列出來讓你選擇——你總是一下得到全部。
如果我們承認(rèn)Promise一般來說要比它的非Promise揩局,不可靠的回調(diào)等價物 慢一點兒——假定在有些地方你覺得你可以自己調(diào)整可靠性的缺失——難道這意味著Promise應(yīng)當(dāng)被全面地避免毫玖,就好像你的整個應(yīng)用程序僅僅由一些可能的“必須絕對最快”的代碼驅(qū)動著?
捫心自問:如果你的代碼有那么合理,那么 對于這樣的任務(wù)孕豹,JavaScript是正確的選擇嗎涩盾? 為了運行應(yīng)用程序JavaScript可以被優(yōu)化得十分高效(參見第五章和第六章)。但是在Promise提供的所有好處的光輝之下励背,過于沉迷它微小的性能權(quán)衡春霍,真的 合適嗎?
另一個微妙的問題是Promise使 所有事情 都成為異步的叶眉,這意味著有些應(yīng)當(dāng)立即完成的(同步的)步驟也要推遲到下一個Job步驟中(參見第一章)址儒。也就是說一個Promise任務(wù)序列要比使用回調(diào)連接的相同序列要完成的稍微慢一些是可能的。
當(dāng)然衅疙,這里的問題是:這些關(guān)于性能的微小零頭的潛在疏忽莲趣,和我們在本章通篇闡述的Promise帶來的益處相比,還值得考慮嗎饱溢?
我的觀點是喧伞,在幾乎所有你可能認(rèn)為Promise的性能慢到了需要被考慮的情況下,完全回避Promise并將它的可靠性和組合性優(yōu)化掉绩郎,實際上一種反模式潘鲫。
相反地,你應(yīng)當(dāng)默認(rèn)地在代碼中廣泛使用它們肋杖,然后再記錄并分析你的應(yīng)用程序的熱(關(guān)鍵)路徑溉仑。Promise 真的 是瓶頸?還是它們只是理論上慢了下來状植?只有在那 之后浊竟,拿著實際合法的基準(zhǔn)分析觀測數(shù)據(jù)(參見第六章),再將Promise從這些關(guān)鍵區(qū)域中重構(gòu)移除才稱得上是合理與謹(jǐn)慎津畸。
Promise是有一點兒慢,但作為交換你得到了很多內(nèi)建的可靠性,無Zalgo的可預(yù)測性徘郭,與組合性抱环。也許真正的限制不是它們的性能,而是你對它們的益處缺乏認(rèn)識因宇?
復(fù)習(xí)
Promise很牛。用它們。它們解決了肆虐在回調(diào)代碼中的 控制倒轉(zhuǎn) 問題。
它們沒有擺脫回調(diào)撩笆,而是重新定向了這些回調(diào)的組織安排方式,是它成為一種坐落于我們和其他工具之間的可靠的中間機制掺涛。
Promise鏈還開始以順序的風(fēng)格定義了一種更好的(當(dāng)然疼电,還不完美)表達異步流程的方式刊苍,它幫我們的大腦更好的規(guī)劃和維護異步JS代碼。我們會在下一章中看到一個更好的解決 這個 問題的方法庭惜!