你不懂JS: 異步與性能 第三章: Promise(下)

官方中文版原文鏈接

感謝社區(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炼铩)般卑,為什么我們的錯誤處理器沒有得到通知?正如我們早先解釋的爽雄,這是因為 這個 錯誤處理器是為ppromise準(zhǔn)備的蝠检,也就是已經(jīng)被值42完成的那個promise。ppromise是不可變的挚瘟,所以唯一可以得到錯誤通知的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)”需要至少一個“選手”革为,所以如果你傳入一個空的arrayrace([..])的主Promise將不會立即解析舵鳞,反而是永遠(yuǎn)不會被解析震檩。這是砸自己的腳!ES6應(yīng)當(dāng)將它規(guī)范為要么完成系任,要么拒絕恳蹲,或者要么拋出某種同步錯誤。不幸的是俩滥,因為在ES6的Promise之前的Promise庫的優(yōu)先權(quán)高,他們不得不把這個坑留在這兒贺奠,所以要小心絕不要傳入一個空array霜旧。

讓我們重溫剛才的并發(fā)Ajax的例子,但是在p1p2競合的環(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)在本章中零散地展開的ES6PromiseAPI绳泉。

注意: 下面的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的PromiseAPI的靜態(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的PromiseAPI十分簡單和直接抓艳。對服務(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)建一個包裝值(比如objectarray)來包含這些多個消息凳谦。這個方法好用忆畅,但是在你的Promise鏈的每一步上把消息包裝再拆開顯得十分尷尬和煩人。

分割值

有時你可以將這種情況當(dāng)做一個信號尸执,表示你可以/應(yīng)當(dāng)將問題拆分為兩個或更多的Promise家凯。

想象你有一個工具foo(..),它異步地產(chǎn)生兩個值(xy):

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(..)返回的東西绊诲,以便于我們不必再將xy包裝進一個單獨的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)在它更易于在未來將xy的計算分開,重構(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代碼。我們會在下一章中看到一個更好的解決 這個 問題的方法庭惜!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末透绩,一起剝皮案震驚了整個濱河市志鞍,隨后出現(xiàn)的幾起案子统翩,更是在濱河造成了極大的恐慌汁汗,老刑警劉巖角寸,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異扁藕,居然都是意外死亡沮峡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門亿柑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來邢疙,“玉大人,你說我怎么就攤上這事望薄∨庇危” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵式矫,是天一觀的道長。 經(jīng)常有香客問我役耕,道長采转,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任瞬痘,我火速辦了婚禮故慈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘框全。我一直安慰自己察绷,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布津辩。 她就那樣靜靜地躺著拆撼,像睡著了一般容劳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上闸度,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天竭贩,我揣著相機與錄音,去河邊找鬼莺禁。 笑死留量,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哟冬。 我是一名探鬼主播楼熄,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼浩峡!你這毒婦竟也來了可岂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤红符,失蹤者是張志新(化名)和其女友劉穎青柄,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體预侯,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡致开,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了萎馅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片双戳。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖糜芳,靈堂內(nèi)的尸體忽然破棺而出飒货,到底是詐尸還是另有隱情,我是刑警寧澤峭竣,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布塘辅,位于F島的核電站,受9級特大地震影響皆撩,放射性物質(zhì)發(fā)生泄漏扣墩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一扛吞、第九天 我趴在偏房一處隱蔽的房頂上張望呻惕。 院中可真熱鬧,春花似錦滥比、人聲如沸亚脆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽濒持。三九已至键耕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間弥喉,已是汗流浹背郁竟。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留由境,地道東北人棚亩。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像虏杰,于是被迫代替她去往敵國和親讥蟆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容