第三章: Promises 1

特別說明迟赃,為便于查閱震桶,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS

你不懂JS: 異步與性能

第三章: Promises

在第二章中纵穿,我們定位了在使用回調(diào)表達(dá)程序異步性和管理并發(fā)的兩個主要類別的不足:缺乏順序性和缺乏可靠性〗雠遥現(xiàn)在我們更親近地理解了問題淳衙,是時候?qū)⑽覀兊淖⒁饬D(zhuǎn)向解決它們的模式了署隘。

我們首先想要解決的是 控制倒轉(zhuǎn) 問題宠能,信任是如此脆弱而且是如此的容易丟失。

回想一下磁餐,我們將我們的程序的延續(xù)包裝進(jìn)一個回調(diào)函數(shù)中棍潘,將這個回調(diào)交給另一個團(tuán)體(甚至是潛在的外部代碼),并雙手合十祈禱它會做正確的事情并調(diào)用這個回調(diào)崖媚。

我們這么做是因為我們想說亦歉,“這是 稍后 將要發(fā)生的事,在當(dāng)前的步驟完成之后畅哑‰瓤”

但是如果我們能夠反向倒轉(zhuǎn)這種 控制倒轉(zhuǎn) 呢?如果不是將我們程序的延續(xù)交給另一個團(tuán)體荠呐,而是希望它返回給我們一個可以知道它何時完成的能力赛蔫,然后我們的代碼可以決定下一步做什么呢?

這種規(guī)范被稱為 Promise泥张。

Promise正在像風(fēng)暴一樣席卷JS世界呵恢,因為開發(fā)者和語言規(guī)范作者之流拼命地想要在他們的代碼/設(shè)計中結(jié)束回調(diào)地獄的瘋狂。事實(shí)上媚创,大多數(shù)新被加入JS/DOM平臺的異步API都是建立在Promise之上的渗钉。所以深入學(xué)習(xí)它們可能是個好主意,你不這么認(rèn)為嗎?

注意: “立即”這個詞將在本章頻繁使用鳄橘,一般來說它指代一些Promise解析行為声离。然而,本質(zhì)上在所有情況下瘫怜,“立即”意味著就工作隊列行為(參見第一章)而言术徊,不是嚴(yán)格同步的 現(xiàn)在 的感覺。

什么是Promise鲸湃?

當(dāng)開發(fā)者們決定要學(xué)習(xí)一種新技術(shù)或模式的時候赠涮,他們的第一步總是“給我看代碼!”暗挑。摸著石頭過河對我們來講是十分自然的世囊。

但事實(shí)上僅僅考察API丟失了一些抽象過程。Promise是這樣一種工具:它能非常明顯地看出使用者是否理解了它是為什么和關(guān)于什么窿祥,還是僅僅學(xué)習(xí)和使用API株憾。

所以在我展示Promise的代碼之前,我想在概念上完整地解釋一下Promise到底是什么晒衩。我希望這能更好地指引你探索如何將Promise理論整合到你自己的異步流程中嗤瞎。

帶著這樣的想法,讓我們來看兩種類比听系,來解釋Promise是什么贝奇。

未來的值

想象這樣的場景:我走到快餐店的柜臺前,點(diǎn)了一個起士漢堡靠胜。并交了1.47美元的現(xiàn)金掉瞳。通過點(diǎn)餐和付款,我為得到一個 (起士漢堡)制造了一個請求浪漠。我發(fā)起了一個事務(wù)陕习。

但是通常來說,起士漢堡不會立即到我手中址愿。收銀員交給一些東西代替我的起士漢堡:一個帶有點(diǎn)餐排隊號的收據(jù)该镣。這個點(diǎn)餐號是一個“我欠你”的許諾(Promise),它保證我最終會得到我的起士漢堡响谓。

于是我就拿著我的收據(jù)和點(diǎn)餐號损合。我知道它代表我的 未來的起士漢堡,所以我無需再擔(dān)心它——除了挨餓娘纷!

在我等待的時候嫁审,我可以做其他的事情,比如給我的朋友發(fā)微信說赖晶,“嘿律适,一塊兒吃午餐嗎?我要吃起士漢堡”。

我已經(jīng)在用我的 未來的起士漢堡 進(jìn)行推理了擦耀,即便它還沒有到我手中棉圈。我的大腦可以這么做是因為它將點(diǎn)餐號作為起士漢堡的占位符號涩堤。這個占位符號實(shí)質(zhì)上使這個值 與時間無關(guān)眷蜓。它是一個 未來的值

最終胎围,我聽到吁系,“113號!”白魂。于是我愉快地拿著收據(jù)走回柜臺前汽纤。我把收據(jù)遞給收銀員,拿回我的起士漢堡福荸。

換句話說蕴坪,一旦我的 未來的值 準(zhǔn)備好,我就用我的許諾值換回值本身敬锐。

但還有另外一種可能的輸出背传。它們叫我的號,但當(dāng)我去取起士漢堡時台夺,收銀員遺憾地告訴我径玖,“對不起,看起來我們的起士漢堡賣光了颤介∈嵝牵”把這種場景下顧客有多沮喪放在一邊,我們可以看到 未來的值 的一個重要性質(zhì):它們既可以表示成功也可以表示失敗滚朵。

每次我點(diǎn)起士漢堡時冤灾,我都知道我要么最終得到一個起士漢堡,要么得到起士漢堡賣光的壞消息辕近,并且不得不考慮中午吃點(diǎn)兒別的東西瞳购。

注意: 在代碼中,事情沒有這么簡單亏推,因為還隱含著一種點(diǎn)餐號永遠(yuǎn)也不會被叫到的情況学赛,這時我們就被擱置在了一種無限等待的未解析狀態(tài)。我們待會兒再回頭處理這種情況吞杭。

現(xiàn)在和稍后的值

這一切也許聽起來在思維上太過抽象而不能實(shí)施在你的代碼中盏浇。那么,讓我們更具體一些芽狗。

然而绢掰,在我們能介紹Promise是如何以這種方式工作之前,我們先看看我們已經(jīng)明白的代碼——回調(diào)!——是如何處理這些 未來值 的滴劲。

在你寫代碼來推導(dǎo)一個值時攻晒,比如在一個number上進(jìn)行數(shù)學(xué)操作,不論你是否理解班挖,對于這個值你已經(jīng)假設(shè)了某些非陈衬螅基礎(chǔ)的事實(shí)——這個值已經(jīng)是一個實(shí)在的 現(xiàn)在 值:

var x, y = 2;

console.log( x + y ); // NaN  <-- 因為`x`還沒有被賦值

x + y操作假定xy都已經(jīng)被設(shè)定好了。用我們一會將要闡述的術(shù)語來講萧芙,我們假定xy的值已經(jīng)被 解析(resovle) 了给梅。

期盼+操作符本身能夠魔法般地檢測并等待xy的值被解析(也就是準(zhǔn)備好),然后僅在那之后才進(jìn)行操作是沒道理的双揪。如果不同的語句 現(xiàn)在 完成而其他的 稍后 完成动羽,這就會在程序中造成混亂,對吧渔期?

如果兩個語句中的一個(或兩者同時)可能還沒有完成运吓,你如何才能推斷它們的關(guān)系呢?如果語句2要依賴語句1的完成疯趟,那么這里僅有兩種輸出:不是語句1 現(xiàn)在 立即完成而且一切處理正常進(jìn)行拘哨,就是語句1還沒有完成,所以語句2將會失敗迅办。

如果這些東西聽起來很像第一章的內(nèi)容宅静,很好!

回到我們的x + y的數(shù)學(xué)操作站欺。想象有一種方法可以說姨夹,“將xy相加,但如果它們中任意一個還沒有被設(shè)置矾策,就等到它們都被設(shè)置磷账。盡快將它們相加〖炙洌”

你的大腦也許剛剛跳進(jìn)回調(diào)逃糟。好吧,那么...

function add(getX,getY,cb) {
    var x, y;
    getX( function(xVal){
        x = xVal;
        // 兩者都準(zhǔn)備好了蓬豁?
        if (y != undefined) {
            cb( x + y );    // 發(fā)送加法的結(jié)果
        }
    } );
    getY( function(yVal){
        y = yVal;
        // 兩者都準(zhǔn)備好了绰咽?
        if (x != undefined) {
            cb( x + y );    // 發(fā)送加法的結(jié)果
        }
    } );
}

// `fetchX()`和`fetchY()`是同步或異步的函數(shù)
add( fetchX, fetchY, function(sum){
    console.log( sum ); // 很簡單吧?
} );

花點(diǎn)兒時間來感受一下這段代碼的美妙(或者丑陋)地粪,我耐心地等你取募。

雖然丑陋是無法否認(rèn)的,但是關(guān)于這種異步模式有一些非常重要的事情需要注意蟆技。

在這段代碼中玩敏,我們將xy作為未來的值對待斗忌,我們將add(..)操作表達(dá)為:(從外部看來)它并不關(guān)心xy或它們兩者現(xiàn)在是否可用。換句話所旺聚,它泛化了 現(xiàn)在稍后织阳,如此我們可以信賴add(..)操作的一個可預(yù)測的結(jié)果。

通過使用一個臨時一致的add(..)——它跨越 現(xiàn)在稍后 的行為是相同的——異步代碼的推理變得容易的多了砰粹。

更直白地說:為了一致地處理 現(xiàn)在稍后唧躲,我們將它們都作為 稍后:所有的操作都變成異步的。

當(dāng)然伸眶,這種粗略的基于回調(diào)的方法留下了許多提升的空間惊窖。為了理解在不用關(guān)心 未來的值 在時間上什么時候變得可用的情況下推理它而帶來的好處刽宪,這僅僅是邁出的一小步厘贼。

Promise值

我們絕對會在本章的后面深入更多關(guān)于Promise的細(xì)節(jié)——所以如果這讓你犯糊涂,不要擔(dān)心——但讓我們先簡單地看一下我們?nèi)绾瓮ㄟ^Promise來表達(dá)x + y的例子:

function add(xPromise,yPromise) {
    // `Promise.all([ .. ])`接收一個Promise的數(shù)組圣拄,
    // 并返回一個等待它們?nèi)客瓿傻男翽romise
    return Promise.all( [xPromise, yPromise] )

    // 當(dāng)這個Promise被解析后嘴秸,我們拿起收到的`X`和`Y`的值,并把它們相加
    .then( function(values){
        // `values`是一個從先前被解析的Promise那里收到的消息數(shù)組
        return values[0] + values[1];
    } );
}

// `fetchX()`和`fetchY()`分別為它們的值返回一個Promise庇谆,
// 這些值可能在 *現(xiàn)在* 或 *稍后* 準(zhǔn)備好
add( fetchX(), fetchY() )

// 為了將兩個數(shù)字相加岳掐,我們得到一個Promise。
// 現(xiàn)在我們鏈?zhǔn)降卣{(diào)用`then(..)`來等待返回的Promise被解析
.then( function(sum){
    console.log( sum ); // 這容易多了饭耳!
} );

在這個代碼段中有兩層Promise串述。

fetchX()fetchY()被直接調(diào)用,它們的返回值(promiseDぁ)被傳入add(..)纲酗。這些promise表示的值將在 現(xiàn)在稍后 準(zhǔn)備好,但是每個promise都將行為泛化為與時間無關(guān)新蟆。我們以一種時間無關(guān)的方式來推理XY的值觅赊。它們是 未來值

第二層是由add(..)創(chuàng)建(通過Promise.all([ .. ]))并返回的promise琼稻,我們通過調(diào)用then(..)來等待它吮螺。當(dāng)add(..)操作完成后,我們的sum未來值 就準(zhǔn)備好并可以打印了帕翻。我們將等待XY未來值 的邏輯隱藏在add(..)內(nèi)部鸠补。

注意:add(..)內(nèi)部。Promise.all([ .. ])調(diào)用創(chuàng)建了一個promise(它在等待promiseXpromiseY被解析)嘀掸。鏈?zhǔn)秸{(diào)用.then(..)創(chuàng)建了另一個promise紫岩,它的return values[0] + values[1]這一行會被立即解析(使用加法的結(jié)果)。這樣横殴,我們鏈接在add(..)調(diào)用末尾的then(..)調(diào)用——在代碼段最后——實(shí)際上是在第二個被返回的promise上進(jìn)行操作被因,而非被Promise.all([ .. ])創(chuàng)建的第一個promise卿拴。另外,雖然我們沒有在這第二個then(..)的末尾鏈接任何操作梨与,它也已經(jīng)創(chuàng)建了另一個promise堕花,我們可以選擇監(jiān)聽/使用它。這類Promise鏈的細(xì)節(jié)將會在本章后面進(jìn)行講解粥鞋。

就像點(diǎn)一個起士漢堡缘挽,Promise的解析可能是一個拒絕(rejection)而非完成(fulfillment)。不同的是呻粹,被完成的Promise的值總是程序化的壕曼,而一個拒絕值——通常被稱為“拒絕理由”——既可以被程序邏輯設(shè)置,也可以被運(yùn)行時異常隱含地設(shè)置等浊。

使用Promise腮郊,then(..)調(diào)用實(shí)際上可以接受兩個函數(shù),第一個用作完成(正如剛才所示)筹燕,而第二個用作拒絕:

add( fetchX(), fetchY() )
.then(
    // 完成處理器
    function(sum) {
        console.log( sum );
    },
    // 拒絕處理器
    function(err) {
        console.error( err ); // 倒霉轧飞!
    }
);

如果在取得XY時出現(xiàn)了錯誤,或在加法操作時某些事情不知怎地失敗了撒踪,add(..)返回的promise就被拒絕了过咬,傳入then(..)的第二個錯誤處理回調(diào)函數(shù)會從promise那里收到拒絕的值。

因為Promise包裝了時間相關(guān)的狀態(tài)——等待當(dāng)前值的完成或拒絕——從外部看來制妄,Promise本身是時間無關(guān)的掸绞,如此Promise就可以用可預(yù)測的方式組合,而不用關(guān)心時間或底層的結(jié)果耕捞。

另外衔掸,一旦Promise被解析,它就永遠(yuǎn)保持那個狀態(tài)——它在那個時刻變成了一個 不可變的值——而且可以根據(jù)需要 被監(jiān)聽 任意多次砸脊。

注意: 因為Promise一旦被解析就是外部不可變的具篇,所以現(xiàn)在將這個值傳遞給任何其他團(tuán)體都是安全的,而且我們知道它不會被意外或惡意地被修改凌埂。這在許多團(tuán)體監(jiān)聽同一個Promise的解析時特別有用驱显。一個團(tuán)體去影響另一個團(tuán)體對Promise解析的監(jiān)聽能力是不可能的。不可變性聽起來是一個學(xué)院派話題瞳抓,但它實(shí)際上是Promise設(shè)計中最基礎(chǔ)且最重要的方面之一埃疫,因此不能將它隨意地跳過。

這是用于理解Promise的最強(qiáng)大且最重要的概念之一孩哑。通過大量的工作栓霜,你可以僅僅使用丑陋的回調(diào)組合來創(chuàng)建相同的效果,但這真的不是一個高效的策略横蜒,特別是你不得不一遍一遍地重復(fù)它胳蛮。

Promise是一種用來包裝與組合 未來值销凑,并且可以很容易復(fù)用的機(jī)制。

完成事件

正如我們剛才看到的仅炊,一個獨(dú)立的Promise作為一個 未來值 動作斗幼。但還有另外一種方式考慮Promise的解析:在一個異步任務(wù)的兩個或以上步驟中,作為一種流程控制機(jī)制——俗稱“這個然后那個”抚垄。

讓我們想象調(diào)用foo(..)來執(zhí)行某個任務(wù)鸡岗。我們對它的細(xì)節(jié)一無所知魏宽,我們也不關(guān)心冠摄。它可能會立即完成任務(wù)鹊漠,也可能會花一段時間完成。

我們僅僅想簡單地知道foo(..)什么時候完成浙滤,以便于我們可以移動到下一個任務(wù)阴挣。換句話說,我們想要一種方法被告知foo(..)的完成瓷叫,以便于我們可以 繼續(xù)屯吊。

在典型的JavaScript風(fēng)格中送巡,如果你需要監(jiān)聽一個通知摹菠,你很可能會想到事件(event)。那么我們可以將我們的通知需求重新表述為骗爆,監(jiān)聽由foo(..)發(fā)出的 完成(或 繼續(xù))事件次氨。

注意: 將它稱為一個“完成事件”還是一個“繼續(xù)事件”取決于你的角度。你是更關(guān)心foo(..)發(fā)生的事情摘投,還是更關(guān)心foo(..)完成 之后 發(fā)生的事情煮寡?兩種角度都對而且都有用。事件通知告訴我們foo(..)已經(jīng) 完成犀呼,但是 繼續(xù) 到下一個步驟也沒問題幸撕。的確,你為了事件通知調(diào)用而傳入的回調(diào)函數(shù)本身外臂,在前面我們稱它為一個 延續(xù)坐儿。因為 完成事件 更加聚焦于foo(..),也就是我們當(dāng)前注意的東西宋光,所以在這篇文章的其余部分我們稍稍偏向于使用 完成事件貌矿。

使用回調(diào),“通知”就是被任務(wù)(foo(..))調(diào)用的我們的回調(diào)函數(shù)罪佳。但是使用Promise逛漫,我們將關(guān)系扭轉(zhuǎn)過來,我們希望能夠監(jiān)聽一個來自于foo(..)的事件赘艳,當(dāng)我們被通知時酌毡,做相應(yīng)的處理克握。

首先,考慮一些假想代碼:

foo(x) {
    // 開始做一些可能會花一段時間的事情
}

foo( 42 )

on (foo "completion") {
    // 現(xiàn)在我們可以做下一步了枷踏!
}

on (foo "error") {
    // 噢玛荞,在`foo(..)`中有某些事情搞錯了
}

我們調(diào)用foo(..)然后我們設(shè)置兩個事件監(jiān)聽器,一個給"completion"呕寝,一個給"error"——foo(..)調(diào)用的兩種可能的最終結(jié)果勋眯。實(shí)質(zhì)上,foo(..)甚至不知道調(diào)用它的代碼監(jiān)聽了這些事件下梢,這構(gòu)成了一個非常美妙的 關(guān)注分離(separation of concerns)客蹋。

不幸的是,這樣的代碼將需要JS環(huán)境不具備的一些“魔法”(而且顯得有些不切實(shí)際)孽江。這里是一種用JS表達(dá)它的更自然的方式:

function foo(x) {
    // 開始做一些可能會花一段時間的事情

    // 制造一個`listener`事件通知能力并返回

    return listener;
}

var evt = foo( 42 );

evt.on( "completion", function(){
    // 現(xiàn)在我們可以做下一步了讶坯!
} );

evt.on( "failure", function(err){
    // 噢,在`foo(..)`中有某些事情搞錯了
} );

foo(..)明確地創(chuàng)建并返回了一個事件監(jiān)聽能力岗屏,調(diào)用方代碼接收并在它上面注冊了兩個事件監(jiān)聽器辆琅。

很明顯這反轉(zhuǎn)了一般的面向回調(diào)代碼,而且是有意為之这刷。與將回調(diào)傳入foo(..)相反婉烟,它返回一個我們稱之為evt的事件能力,它接收回調(diào)暇屋。

但如果你回想第二章似袁,回調(diào)本身代表著一種 控制反轉(zhuǎn)。所以反轉(zhuǎn)回調(diào)模式實(shí)際上是 反轉(zhuǎn)的反轉(zhuǎn)咐刨,或者說是一個 控制非反轉(zhuǎn)——將控制權(quán)歸還給我們希望保持它的調(diào)用方代碼昙衅,

一個重要的好處是,代碼的多個分離部分都可以被賦予事件監(jiān)聽能力定鸟,而且它們都可在foo(..)完成時被獨(dú)立地通知而涉,來執(zhí)行后續(xù)的步驟:

var evt = foo( 42 );

// 讓`bar(..)`監(jiān)聽`foo(..)`的完成
bar( evt );

// 同時,讓`baz(..)`監(jiān)聽`foo(..)`的完成
baz( evt );

控制非反轉(zhuǎn) 導(dǎo)致了更好的 關(guān)注分離联予,也就是bar(..)baz(..)不必卷入foo(..)是如何被調(diào)用的問題啼县。相似地,foo(..)也不必知道或關(guān)心bar(..)baz(..)的存在或它們是否在等待foo(..)完成的通知躯泰。

實(shí)質(zhì)上谭羔,這個evt對象是一個中立的第三方團(tuán)體,在分離的關(guān)注點(diǎn)之間進(jìn)行交涉麦向。

Promise“事件”

正如你可能已經(jīng)猜到的瘟裸,evt事件監(jiān)聽能力是一個Promise的類比。

在一個基于Promise的方式中诵竭,前面的代碼段將會使foo(..)創(chuàng)建并返回一個Promise實(shí)例话告,而且這個promise將會被傳入bar(..)baz(..)兼搏。

注意: 我們監(jiān)聽的Promise解析“事件”并不是嚴(yán)格的事件(雖然它們?yōu)榱四承┠康谋憩F(xiàn)得像事件),而且它們也不經(jīng)常稱為"completion""error"沙郭。相反佛呻,我們用then(..)來注冊一個"then"事件〔∠撸或者也許更準(zhǔn)確地講吓著,then(..)注冊了"fulfillment(完成)"和/或"rejection(拒絕)"事件,雖然我們在代碼中不會看到這些名詞被明確地使用送挑。

考慮:

function foo(x) {
    // 開始做一些可能會花一段時間的事情

    // 構(gòu)建并返回一個promise
    return new Promise( function(resolve,reject){
        // 最終需要調(diào)用`resolve(..)`或`reject(..)`
        // 它們是這個promise的解析回調(diào)
    } );
}

var p = foo( 42 );

bar( p );

baz( p );

注意:new Promise( function(..){ .. } )中展示的模式通常被稱為“揭示構(gòu)造器(revealing constructor)”绑莺。被傳入的函數(shù)被立即執(zhí)行(不會被異步推遲,像then(..)的回調(diào)那樣)惕耕,而且它被提供了兩個參數(shù)纺裁,我們叫它們resolvereject。這些是Promise的解析函數(shù)司澎。resolve(..)一般表示完成欺缘,而reject(..)表示拒絕。

你可能猜到了bar(..)baz(..)的內(nèi)部看起來是什么樣子:

function bar(fooPromise) {
    // 監(jiān)聽`foo(..)`的完成
    fooPromise.then(
        function(){
            // `foo(..)`現(xiàn)在完成了挤安,那么做`bar(..)`的任務(wù)
        },
        function(){
            // 噢谚殊,在`foo(..)`中有某些事情搞錯了
        }
    );
}

// `baz(..)`同上

Promise解析沒有必要一定發(fā)送消息,就像我們將Promise作為 未來值 考察時那樣漱受。它可以僅僅作為一種流程控制信號络凿,就像前面的代碼中那樣使用。

另一種表達(dá)方式是:

function bar() {
    // `foo(..)`絕對已經(jīng)完成了昂羡,那么做`bar(..)`的任務(wù)
}

function oopsBar() {
    // 噢,在`foo(..)`中有某些事情搞錯了摔踱,那么`bar(..)`不會運(yùn)行
}

// `baz()`和`oopsBaz()`同上

var p = foo( 42 );

p.then( bar, oopsBar );

p.then( baz, oopsBaz );

注意: 如果你以前見過基于Promise的代碼虐先,你可能會相信這段代碼的最后兩行應(yīng)當(dāng)寫做p.then( .. ).then( .. ),使用鏈接派敷,而不是p.then(..); p.then(..)蛹批。這將會是兩種完全不同的行為,所以要小心篮愉!這種區(qū)別現(xiàn)在看起來可能不明顯腐芍,但是它們實(shí)際上是我們目前還沒有見過的異步模式:分割(splitting)/分叉(forking)。不必?fù)?dān)心试躏!本章后面我們會回到這個話題猪勇。

與將ppromise傳入bar(..)baz(..)相反,我們使用promise來控制bar(..)baz(..)何時該運(yùn)行颠蕴,如果有這樣的時刻泣刹。主要區(qū)別在于錯誤處理助析。

在第一個代碼段的方式中,無論foo(..)是否成功bar(..)都會被調(diào)用椅您,如果被通知foo(..)失敗了的話它提供自己的后備邏輯外冀。顯然,baz(..)也是這樣做的掀泳。

在第二個代碼段中雪隧,bar(..)僅在foo(..)成功后才被調(diào)用,否則oopsBar(..)會被調(diào)用员舵。baz(..)也是膀跌。

兩種方式本身都 。但會有一些情況使一種優(yōu)于另一種固灵。

在這兩種方式中捅伤,從foo(..)返回的promisep都被用于控制下一步發(fā)生什么。

另外巫玻,兩個代碼段都以對同一個promisep調(diào)用兩次then(..)結(jié)束丛忆,這展示了先前的觀點(diǎn),也就是Promise(一旦被解析)會永遠(yuǎn)保持相同的解析結(jié)果(完成或拒絕)仍秤,而且可以按需要后續(xù)地被監(jiān)聽任意多次熄诡。

無論何時p被解析,下一步都將總是相同的诗力,包括 現(xiàn)在稍后凰浮。

Thenable鴨子類型(Duck Typing)

在Promise的世界中,一個重要的細(xì)節(jié)是如何確定一個值是否是純粹的Promise苇本⊥嗉耄或者更直接地說,一個值會不會像Promise那樣動作瓣窄?

我們知道Promise是由new Promise(..)語法構(gòu)建的笛厦,你可能會想p instanceof Promise將是一個可以接受的檢查。但不幸的是俺夕,有幾個理由表明它不是完全夠用裳凸。

主要原因是,你可以從其他瀏覽器窗口中收到Promise值(iframe等)劝贸,其他的瀏覽器窗口會擁有自己的不同于當(dāng)前窗口/frame的Promise姨谷,這種檢查將會在定位Promise實(shí)例時失效。

另外映九,一個庫或框架可能會選擇實(shí)現(xiàn)自己的Promise而不是用ES6原生的Promise實(shí)現(xiàn)梦湘。事實(shí)上,你很可能在根本沒有Promise的老版本瀏覽器中通過一個庫來使用Promise。

當(dāng)我們在本章稍后討論P(yáng)romise的解析過程時践叠,為什么識別并同化一個非純種但相似Promise的值仍然很重要會愈發(fā)明顯言缤。但目前只需要相信我,它是拼圖中很重要的一塊禁灼。

如此管挟,人們決定識別一個Promise(或像Promise一樣動作的某些東西)的方法是定義一種稱為“thenable”的東西,也就是任何擁有then(..)方法的對象或函數(shù)弄捕。這種方法假定任何這樣的值都是一個符合Promise的thenable僻孝。

根據(jù)值的形狀(存在什么屬性)來推測它的“類型”的“類型檢查”有一個一般的名稱,稱為“鴨子類型檢查”——“如果它看起來像一只鴨子守谓,并且叫起來像一只鴨子穿铆,那么它一定是一只鴨子”(參見本叢書的 類型與文法)。所以對thenable的鴨子類型檢查可能大致是這樣:

if (
    p !== null &&
    (
        typeof p === "object" ||
        typeof p === "function"
    ) &&
    typeof p.then === "function"
) {
    // 認(rèn)為它是一個thenable!
}
else {
    // 不是一個thenable
}

暈斋荞!先把將這種邏輯在各種地方實(shí)現(xiàn)有點(diǎn)丑陋的事實(shí)放在一邊不談荞雏,這里還有更多更深層的麻煩。

如果你試著用一個偶然擁有then(..)函數(shù)的任意對象/函數(shù)來完成一個Promise平酿,但你又沒想把它當(dāng)做一個Promise/thenable來對待凤优,你的運(yùn)氣就用光了,因為它會被自動地識別為一個thenable并以特殊的規(guī)則來對待(見本章后面的部分)蜈彼。

如果你不知道一個值上面擁有then(..)就更是這樣筑辨。比如:

var o = { then: function(){} };

// 使`v`用`[[Prototype]]`鏈接到`o`
var v = Object.create( o );

v.someStuff = "cool";
v.otherStuff = "not so cool";

v.hasOwnProperty( "then" );     // false

v看起來根本不像是一個Promise或thenable。它只是一個擁有一些屬性的直白的對象幸逆。你可能只是想要把這個值像其他對象那樣傳遞而已棍辕。

但你不知道的是,v[[Prototype]]連接著(見本叢書的 this與對象原型)另一個對象o还绘,在它上面偶然擁有一個then(..)楚昭。所以thenable鴨子類型檢查將會認(rèn)為并假定v是一個thenable。噢蚕甥。

它甚至不需要直接故意那么做:

Object.prototype.then = function(){};
Array.prototype.then = function(){};

var v1 = { hello: "world" };
var v2 = [ "Hello", "World" ];

v1v2都將被假定為是thenalbe的哪替。你不能控制或預(yù)測是否有其他代碼偶然或惡意地將then(..)加到Object.prototypeArray.prototype菇怀,或其他任何原生原型上。而且如果這個指定的函數(shù)并不將它的任何參數(shù)作為回調(diào)調(diào)用晌块,那么任何用這樣的值被解析的Promise都將無聲地永遠(yuǎn)掛起爱沟!瘋狂。

聽起來難以置信或不太可能匆背?也許呼伸。

要知道,在ES6之前就有幾種廣為人知的非Promise庫在社區(qū)中存在了,而且它們已經(jīng)偶然擁有了稱為then(..)的方法括享。這些庫中的一些選擇了重命名它們自己的方法來回避沖突(這很爛BЦ)。另一些則因為它們無法改變來回避沖突铃辖,簡單地降級為“不兼容基于Promise的代碼”的不幸狀態(tài)剩愧。

用來劫持原先非保留的——而且聽起來完全是通用的——then屬性名稱的標(biāo)準(zhǔn)決議是,沒有值(或它的任何委托)娇斩,無論是過去仁卷,現(xiàn)在,還是將來犬第,可以擁有then(..)函數(shù)锦积,不管是有意的還是偶然的,否則這個值將在Promise系統(tǒng)中被混淆為一個thenable歉嗓,從而可能產(chǎn)生非常難以追蹤的Bug丰介。

警告: 我不喜歡我們用thenable的鴨子類型來結(jié)束對Promise認(rèn)知的方式。還有其他的選項鉴分,比如“branding”或者甚至是“anti-branding”哮幢;我們得到的似乎是一個最差勁兒的妥協(xié)。但它并不全是悲觀與失望冠场。thenable鴨子類型可以很有用家浇,就像我們馬上要看到的。只是要小心碴裙,如果thenable鴨子類型將不是Promise的東西誤認(rèn)為是Promise钢悲,它就可能成為災(zāi)難。

Promise的信任

我們已經(jīng)看過了兩個強(qiáng)烈的類比舔株,它們解釋了Promise可以為我們的異步代碼所做的事的不同方面莺琳。但如果我們停在這里,我們就可能會錯過一個Promise模式建立的最重要的性質(zhì):信任载慈。

隨著 未來值完成事件 的類別在我們探索的代碼模式中的明確展開惭等,有一個問題依然沒有完全明確:Promise是為什么,以及如何被設(shè)計為來解決所有我們在第二章“信任問題”一節(jié)中提出的 控制倒轉(zhuǎn) 的信任問題的办铡。但是只要深挖一點(diǎn)兒辞做,我們就可以發(fā)現(xiàn)一些重要的保證,來重建第二章中毀掉的對異步代碼的信心寡具!

讓我們從復(fù)習(xí)僅使用回調(diào)的代碼中的信任問題開始秤茅。當(dāng)你傳遞一個回調(diào)給一個工具foo(..)的時候,它可能:

  • 調(diào)用回調(diào)太早
  • 調(diào)用回調(diào)太晚(或根本不調(diào))
  • 調(diào)用回調(diào)太少或太多次
  • 沒能傳遞必要的環(huán)境/參數(shù)
  • 吞掉了任何可能發(fā)生的錯誤/異常

Promise的性質(zhì)被有意地設(shè)計為給這些顧慮提供有用的童叠,可復(fù)用的答案框喳。

調(diào)的太早

這種顧慮主要是代碼是否會引入類Zalgo效應(yīng),也就是一個任務(wù)有時會同步完地成,而有時會異步地完成五垮,這將導(dǎo)致竟合狀態(tài)乍惊。

Promise被定義為不能受這種顧慮的影響,因為即便是立即完成的Promise(比如 new Promise(function(resolve){ resolve(42); }))也不可能被同步地 監(jiān)聽放仗。

也就是說润绎,但你在Promise上調(diào)用then(..)的時候,即便這個Promise已經(jīng)被解析了匙监,你給then(..)提供的回調(diào)也將 總是 被異步地調(diào)用(更多關(guān)于這里的內(nèi)容凡橱,參照第一章的"Jobs")。

不必再插入你自己的setTimeout(..,0)黑科技了亭姥。Promise自動地防止了Zalgo效應(yīng)稼钩。

調(diào)的太晚

和前一點(diǎn)相似,在resolve(..)reject(..)被Promise創(chuàng)建機(jī)制調(diào)用時达罗,一個Promise的then(..)上注冊的監(jiān)聽回調(diào)將自動地被排程坝撑。這些被排程好的回調(diào)將在下一個異步時刻被可預(yù)測地觸發(fā)(參照第一章的"Jobs")。

同步監(jiān)聽是不可能的粮揉,所以不可能有一個同步的任務(wù)鏈的運(yùn)行來“推遲”另一個回調(diào)的發(fā)生巡李。也就是說,當(dāng)一個Promise被解析時扶认,所有在then(..)上注冊的回調(diào)都將被立即侨拦,按順序地,在下一個異步機(jī)會時被調(diào)用(再一次辐宾,參照第一章的"Jobs")狱从,而且沒有任何在這些回調(diào)中發(fā)生的事情可以影響/推遲其他回調(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

這里叠纹,有賴于Promise如何定義操作季研,"C"不可能干擾并優(yōu)先于"B"

Promise排程的怪現(xiàn)象

重要并需要注意的是誉察,排程有許多微妙的地方:鏈接在兩個分離的Promise上的回調(diào)之間的相對順序与涡,是不能可靠預(yù)測的。

如果兩個promisep1p2都準(zhǔn)備好被解析了持偏,那么p1.then(..); p2.then(..)應(yīng)當(dāng)歸結(jié)為首先調(diào)用p1的回調(diào)驼卖,然后調(diào)用p2的。但有一些微妙的情形可能會使這不成立鸿秆,比如下面這樣:

var p3 = new Promise( function(resolve,reject){
    resolve( "B" );
} );

var p1 = new Promise( function(resolve,reject){
    resolve( p3 );
} );

var p2 = new Promise( function(resolve,reject){
    resolve( "A" );
} );

p1.then( function(v){
    console.log( v );
} );

p2.then( function(v){
    console.log( v );
} );

// A B  <-- 不是你可能期望的 B A

我們稍后會更多地講解這個問題款慨,但如你所見,p1不是被一個立即值所解析的谬莹,而是由另一個promisep3所解析,而p3本身被一個值"B"所解析。這種指定的行為將p3展開p1附帽,但是是異步地埠戳,所以在異步工作隊列中p1的回調(diào)位于p2的回調(diào)之后(參照第一章的"Jobs")。

為了回避這樣的微妙的噩夢蕉扮,你絕不應(yīng)該依靠任何跨Promise的回調(diào)順序/排程整胃。事實(shí)上,一個好的實(shí)踐方式是在代碼中根本不要讓多個回調(diào)的順序成為問題喳钟。盡可能回避它屁使。

根本不調(diào)回調(diào)

這是一個很常見的顧慮。Promise用幾種方式解決它奔则。

首先蛮寂,沒有任何東西(JS錯誤都不能)可以阻止一個Promise通知你它的解析(如果它被解析了的話)。如果你在一個Promise上同時注冊了完成和拒絕回調(diào)易茬,而且這個Promise被解析了酬蹋,兩個回調(diào)中的一個總會被調(diào)用。

當(dāng)然抽莱,如果你的回調(diào)本身有JS錯誤范抓,你可能不會看到你期望的結(jié)果,但是回調(diào)事實(shí)上已經(jīng)被調(diào)用了食铐。我們待會兒就會講到如何在你的回調(diào)中收到關(guān)于一個錯誤的通知匕垫,因為就算是它們也不會被吞掉。

那如果Promise本身不管怎樣永遠(yuǎn)沒有被解析呢虐呻?即便是這種狀態(tài)Promise也給出了答案象泵,使用一個稱為“競賽(race)”的高級抽象。

// 一個使Promise超時的工具
function timeoutPromise(delay) {
    return new Promise( function(resolve,reject){
        setTimeout( function(){
            reject( "Timeout!" );
        }, delay );
    } );
}

// 為`foo()`設(shè)置一個超時
Promise.race( [
    foo(),                  // 嘗試調(diào)用`foo()`
    timeoutPromise( 3000 )  // 給它3秒鐘
] )
.then(
    function(){
        // `foo(..)`及時地完成了铃慷!
    },
    function(err){
        // `foo()`不是被拒絕了单芜,就是它沒有及時完成
        // 那么可以考察`err`來知道是哪種情況
    }
);

這種Promise的超時模式有更多的細(xì)節(jié)需要考慮,但我們待會兒再回頭討論犁柜。

重要的是洲鸠,我們可以確保一個信號作為foo(..)的結(jié)果,來防止它無限地掛起我們的程序馋缅。

調(diào)太少或太多次

根據(jù)定義扒腕,對于被調(diào)用的回調(diào)來講 一次 是一個合適的次數(shù)∮┿玻“太少”的情況將會是0次瘾腰,和我們剛剛考察的從不調(diào)用是相同的。

“太多”的情況則很容易解釋覆履。Promise被定義為只能被解析一次蹋盆。如果因為某些原因费薄,Promise的創(chuàng)建代碼試著調(diào)用resolve(..)reject(..)許多次,或者試著同時調(diào)用它們倆栖雾,Promise將僅接受第一次解析楞抡,而無聲地忽略后續(xù)的嘗試。

因為一個Promise僅能被解析一次析藕,所以任何then(..)上注冊的(每個)回調(diào)將僅僅被調(diào)用一次召廷。

當(dāng)然,如果你把同一個回調(diào)注冊多次(比如p.then(f); p.then(f);)账胧,那么它就會被調(diào)用注冊的那么多次竞慢。響應(yīng)函數(shù)僅被調(diào)用一次的保證并不能防止你砸自己的腳。

沒能傳入任何參數(shù)/環(huán)境

Promise可以擁有最多一個解析值(完成或拒絕)治泥。

如果無論怎樣你沒有用一個值明確地解析它筹煮,它的值就是undefined,就像JS中常見的那樣车摄。但不管是什么值寺谤,它總是會被傳入所有被注冊的(并且適當(dāng)?shù)兀和瓿苫蚓芙^)回調(diào)中,不管是 現(xiàn)在 還是將來吮播。

需要意識到的是:如果你使用多個參數(shù)調(diào)用resolve(..)reject(..)变屁,所有第一個參數(shù)之外的后續(xù)參數(shù)都會被無聲地忽略。雖然這看起來違反了我們剛才描述的保證意狠,但并不確切粟关,因為它構(gòu)成了一種Promise機(jī)制的無效使用方式。其他的API無效使用方式(比如調(diào)用resolve(..)許多次)也都相似地 被保護(hù)环戈,所以Promise的行為在這里是一致的(除了有一點(diǎn)點(diǎn)讓人沮喪)闷板。

如果你想傳遞多個值,你必須將它們包裝在另一個單獨(dú)的值中院塞,比如一個array或一個object遮晚。

至于環(huán)境,JS中的函數(shù)總是保持他們被定義時所在作用域的閉包(見本系列的 作用域與閉包)拦止,所以它們理所當(dāng)然地可以繼續(xù)訪問你提供的環(huán)境狀態(tài)县遣。當(dāng)然,這對僅使用回調(diào)的設(shè)計來講也是對的汹族,所以這不能算是Promise帶來的增益——但盡管如此萧求,它依然是我們可以依賴的保證。

吞掉所有錯誤/異常

在基本的感覺上顶瞒,這是前一點(diǎn)的重述夸政。如果你用一個 理由(也就是錯誤消息)拒絕一個Promise,這個值就會被傳入拒絕回調(diào)榴徐。

但是這里有一個更重要的事情守问。如果在Promise的創(chuàng)建過程中的任意一點(diǎn)匀归,或者在監(jiān)聽它的解析的過程中,一個JS異常錯誤發(fā)生的話酪碘,比如TypeErrorReferenceError朋譬,這個異常將會被捕獲,并且強(qiáng)制當(dāng)前的Promise變?yōu)榫芙^兴垦。

舉例來說:

var p = new Promise( function(resolve,reject){
    foo.bar();  // `foo`沒有定義,所以這是一個錯誤字柠!
    resolve( 42 );  // 永遠(yuǎn)不會跑到這里 :(
} );

p.then(
    function fulfilled(){
        // 永遠(yuǎn)不會跑到這里 :(
    },
    function rejected(err){
        // `err`將是一個來自`foo.bar()`那一行的`TypeError`異常對象
    }
);

foo.bar()上發(fā)生的JS異常變成了一個你可以捕獲并響應(yīng)的Promise拒絕探越。

這是一個重要的細(xì)節(jié),因為它有效地解決了另一種潛在的Zalgo時刻窑业,也就是錯誤可能會產(chǎn)生一個同步的反應(yīng)钦幔,而沒有錯誤的部分還是異步的。Promise甚至將JS異常都轉(zhuǎn)化為異步行為常柄,因此極大地降低了發(fā)生竟合狀態(tài)的可能性鲤氢。

但是如果Promise完成了,但是在監(jiān)聽過程中(在一個then(..)上注冊的回調(diào)上)出現(xiàn)了JS異常錯誤會怎樣呢西潘?即便是那些也不會丟失卷玉,但你可能會發(fā)現(xiàn)處理它們的方式有些令人詫異,除非你深挖一些:

var p = new Promise( function(resolve,reject){
    resolve( 42 );
} );

p.then(
    function fulfilled(msg){
        foo.bar();
        console.log( msg ); // 永遠(yuǎn)不會跑到這里 :(
    },
    function rejected(err){
        // 也永遠(yuǎn)不會跑到這里 :(
    }
);

等一下喷市,這看起來foo.bar()發(fā)生的異常確實(shí)被吞掉了相种。不要害怕,它沒有品姓。但更深層次的東西出問題了寝并,也就是我們沒能成功地監(jiān)聽他。p.then(..)調(diào)用本身返回另一個promise腹备,是 那個 promise將會被TypeError異常拒絕衬潦。

為什么它不能調(diào)用我們在這里定義的錯誤處理器呢?表面上看起來是一個符合邏輯的行為植酥。但它會違反Promise一旦被解析就 不可變 的基本原則镀岛。p已經(jīng)完成為值42,所以它不能因為在監(jiān)聽p的解析時發(fā)生了錯誤,而在稍后變成一個拒絕。

除了違反原則搬卒,這樣的行為還可能造成破壞闷旧,假如說有多個在promisep上注冊的then(..)回調(diào),因為有些會被調(diào)用而有些不會忆蚀,而且至于為什么是很明顯的。

可信的Promise?

為了基于Promise模式建立信任买喧,還有最后一個細(xì)節(jié)需要考察捻悯。

無疑你已經(jīng)注意到了,Promise根本沒有擺脫回調(diào)淤毛。它們只是改變了回調(diào)傳遞的位置今缚。與將一個回調(diào)傳入foo(..)相反,我們從foo(..)那里拿回 某些東西 (表面上是一個純粹的Promise)低淡,然后我們將回調(diào)傳入這個 東西姓言。

但為什么這要比僅使用回調(diào)的方式更可靠呢?我們?nèi)绾未_信我們拿回來的 某些東西 事實(shí)上是一個可信的Promise蔗蹋?這難道不是說我們相信它僅僅因為我們已經(jīng)相信它了嗎何荚?

一個Promise經(jīng)常被忽視,但是最重要的細(xì)節(jié)之一猪杭,就是它也為這個問題給出了解決方案餐塘。包含在原生的ES6Promise實(shí)現(xiàn)中,它就是Promise.resolve(..)皂吮。

如果你傳遞一個立即的戒傻,非Promise的,非thenable的值給Promise.resolve(..)蜂筹,你會得到一個用這個值完成的promise需纳。換句話說,下面兩個promisep1p2的行為基本上完全相同:

var p1 = new Promise( function(resolve,reject){
    resolve( 42 );
} );

var p2 = Promise.resolve( 42 );

但如果你傳遞一個純粹的Promise給Promise.resolve(..)狂票,你會得到這個完全相同的promise:

var p1 = Promise.resolve( 42 );

var p2 = Promise.resolve( p1 );

p1 === p2; // true

更重要的是候齿,如果你傳遞一個非Promise的thenable值給Promise.resolve(..),它會試著將這個值展開闺属,而且直到抽出一個最終具體的非Promise值之前慌盯,展開操作將會一直繼續(xù)下去。

還記得我們先前討論的thenable嗎掂器?

考慮這段代碼:

var p = {
    then: function(cb) {
        cb( 42 );
    }
};

// 這工作起來沒問題亚皂,但要靠運(yùn)氣
p
.then(
    function fulfilled(val){
        console.log( val ); // 42
    },
    function rejected(err){
        // 永遠(yuǎn)不會跑到這里
    }
);

這個p是一個thenable,但它不是一個純粹的Promise国瓮。很走運(yùn)灭必,它是合理的,正如大多數(shù)情況那樣乃摹。但是如果你得到的是看起來像這樣的東西:

var p = {
    then: function(cb,errcb) {
        cb( 42 );
        errcb( "evil laugh" );
    }
};

p
.then(
    function fulfilled(val){
        console.log( val ); // 42
    },
    function rejected(err){
        // 噢,這里本不該運(yùn)行
        console.log( err ); // evil laugh
    }
);

這個p是一個thenable播歼,但它不是表現(xiàn)良好的promise掰读。它是惡意的嗎?或者它只是不知道Promise應(yīng)當(dāng)如何工作烁试?老實(shí)說,這不重要靖诗。不管哪種情況辩蛋,它都不那么可靠呻畸。

盡管如此,我們可以將這兩個版本的p傳入Promise.resolve(..)悼院,而且我們將會得到一個我們期望的泛化,安全的結(jié)果:

Promise.resolve( p )
.then(
    function fulfilled(val){
        console.log( val ); // 42
    },
    function rejected(err){
        // 永遠(yuǎn)不會跑到這里
    }
);

Promise.resolve(..)會接受任何thenable咒循,而且將它展開直至非thenable值据途。但你會從Promise.resolve(..)那里得到一個真正的,純粹的Promise叙甸,一個你可以信任的東西颖医。如果你傳入的東西已經(jīng)是一個純粹的Promise了,那么你會單純地將它拿回來裆蒸,所以通過Promise.resolve(..)過濾來得到信任沒有任何壞處熔萧。

那么我們假定,我們在調(diào)用一個foo(..)工具僚祷,而且不能確定我們能相信它的返回值是一個行為規(guī)范的Promise佛致,但我們知道它至少是一個thenable。Promise.resolve(..)將會給我們一個可靠的Promise包裝器來進(jìn)行鏈?zhǔn)秸{(diào)用:

// 不要只是這么做:
foo( 42 )
.then( function(v){
    console.log( v );
} );

// 相反辙谜,這樣做:
Promise.resolve( foo( 42 ) )
.then( function(v){
    console.log( v );
} );

注意: 將任意函數(shù)的返回值(thenable或不是thenable)包裝在Promise.resolve(..)中的另一個好的副作用是俺榆,它可以很容易地將函數(shù)調(diào)用泛化為一個行為規(guī)范的異步任務(wù)。如果foo(42)有時返回一個立即值装哆,而其他時候返回一個Promise罐脊,Promise.resolve(foo(42))萍桌,將確保它總是返回Promise上炎。并且使代碼成為回避Zalgo效應(yīng)的更好的代碼辛块。

信任建立了

希望前面的討論使你現(xiàn)在完全理解了Promise是可靠的,而且更為重要的是尘盼,為什么信任對于建造強(qiáng)壯,可維護(hù)的軟件來說是如此關(guān)鍵午阵。

沒有信任,你能用JS編寫異步代碼嗎籽懦?你當(dāng)然能暮顺。我們JS開發(fā)者在除了回調(diào)以外沒有任何東西的情況下,寫了將近20年的異步代碼了。

但是一旦你開始質(zhì)疑你到底能夠以多大的程度相信你的底層機(jī)制株扛,它實(shí)際上多么可預(yù)見,多么可靠,你就會開始理解回調(diào)的信任基礎(chǔ)多么的搖搖欲墜拦惋。

Promise是一個用可靠語義來增強(qiáng)回調(diào)的模式言秸,所以它的行為更合理更可靠。通過將回調(diào)的 控制倒轉(zhuǎn) 反置過來,我們將控制交給一個可靠的系統(tǒng)(Promise),它是為了將你的異步處理進(jìn)行清晰的表達(dá)而特意設(shè)計的。

鏈?zhǔn)搅鞒?/h2>

我們已經(jīng)被暗示過幾次,但Promise不僅僅是一個單步的 這個然后那個 操作機(jī)制。當(dāng)然撩鹿,那是構(gòu)建塊兒,但事實(shí)證明我們可以將多個Promise串聯(lián)在一起來表達(dá)一系列的異步步驟甫贯。

使這一切能夠工作的關(guān)鍵,是Promise的兩個固有行為:

  • 每次你在一個Promise上調(diào)用then(..)的時候渴逻,它都創(chuàng)建并返回一個新的Promise雪位,我們可以在它上面進(jìn)行 鏈接茧泪。
  • 無論你從then(..)調(diào)用的完成回調(diào)中(第一個參數(shù))返回什么值,它都做為被鏈接的Promise的完成。

我們首先來說明一下這是什么意思锈颗,然后我們將會延伸出它是如何幫助我們創(chuàng)建異步順序的控制流程的「泊迹考慮下面的代碼:

var p = Promise.resolve( 21 );

var p2 = p.then( function(v){
    console.log( v );   // 21

    // 使用值`42`完成`p2`
    return v * 2;
} );

// 在`p2`后鏈接
p2.then( function(v){
    console.log( v );   // 42
} );

通過返回v * 2(也就是42),我們完成了由第一個then(..)調(diào)用創(chuàng)建并返回的p2promise。當(dāng)p2then(..)調(diào)用運(yùn)行時,它從return v * 2語句那里收到完成信號板甘。當(dāng)然寞奸,p2.then(..)還會創(chuàng)建另一個promise猫妙,我們將它存儲在變量p3中齐帚。

但是不得不創(chuàng)建臨時變量p2(或p3等)有點(diǎn)兒惱人。幸運(yùn)的是剪菱,我們可以簡單地將這些鏈接在一起:

var p = Promise.resolve( 21 );

p
.then( function(v){
    console.log( v );   // 21

    // 使用值`42`完成被鏈接的promise
    return v * 2;
} )
// 這里是被鏈接的promise
.then( function(v){
    console.log( v );   // 42
} );

那么現(xiàn)在第一個then(..)是異步序列的第一步,而第二個then(..)就是第二步。它可以根據(jù)你的需要延伸至任意長冻押。只要持續(xù)不斷地用每個自動創(chuàng)建的Promise在前一個then(..)末尾進(jìn)行連接即可。

但是這里錯過了某些東西。要是我們想讓第2步等待第1步去做一些異步的事情呢?我們使用的是一個立即的return語句善延,它立即完成了鏈接中的promise侨歉。

使Promise序列在每一步上都是真正異步的關(guān)鍵,需要回憶一下當(dāng)你向Promise.resolve(..)傳遞一個Promise或thenable而非一個最終值時它如何執(zhí)行颊艳。Promise.resolve(..)會直接返回收到的純粹Promise棋枕,或者它會展開收到的thenable的值——并且它會遞歸地持續(xù)展開thenable。

如果你從完成(或拒絕)處理器中返回一個thenable或Promise窥浪,同樣的展開操作也會發(fā)生漾脂」歉澹考慮這段代碼:

var p = Promise.resolve( 21 );

p.then( function(v){
    console.log( v );   // 21

    // 創(chuàng)建一個promise并返回它
    return new Promise( function(resolve,reject){
        // 使用值`42`完成
        resolve( v * 2 );
    } );
} )
.then( function(v){
    console.log( v );   // 42
} );

即便我們把42包裝在一個我們返回的promise中判呕,它依然會被展開并作為下一個被鏈接的promise的解析哼蛆,如此第二個then(..)仍然收到42腾节。如果我們在這個包裝promise中引入異步耍铜,一切還是會同樣正常的工作:

var p = Promise.resolve( 21 );

p.then( function(v){
    console.log( v );   // 21

    // 創(chuàng)建一個promise并返回
    return new Promise( function(resolve,reject){
        // 引入異步涛酗!
        setTimeout( function(){
            // 使用值`42`完成
            resolve( v * 2 );
        }, 100 );
    } );
} )
.then( function(v){
    // 在上一步中的100毫秒延遲之后運(yùn)行
    console.log( v );   // 42
} );

這真是不可思議的強(qiáng)大聚至!現(xiàn)在我們可以構(gòu)建一個序列骚揍,它可以有我們想要的任意多的步驟,而且每一步都可以按照需要來推遲下一步(或者不推遲)。

當(dāng)然千劈,在這些例子中一步一步向下傳遞的值是可選的。如果你沒有返回一個明確的值谭溉,那么它假定一個隱含的undefined签舞,而且promise依然會以同樣的方式鏈接在一起。如此捶牢,每個Promise的解析只不過是進(jìn)行至下一步的信號帘靡。

為了演示更長的鏈接怯邪,讓我們把推遲Promise的創(chuàng)建(沒有解析信息)泛化為一個我們可以在多個步驟中復(fù)用的工具:

function delay(time) {
    return new Promise( function(resolve,reject){
        setTimeout( resolve, time );
    } );
}

delay( 100 ) // step 1
.then( function STEP2(){
    console.log( "step 2 (after 100ms)" );
    return delay( 200 );
} )
.then( function STEP3(){
    console.log( "step 3 (after another 200ms)" );
} )
.then( function STEP4(){
    console.log( "step 4 (next Job)" );
    return delay( 50 );
} )
.then( function STEP5(){
    console.log( "step 5 (after another 50ms)" );
} )
...

調(diào)用delay(200)創(chuàng)建了一個將在200毫秒內(nèi)完成的promise垒拢,然后我們在第一個then(..)的完成回調(diào)中返回它地沮,這將使第二個then(..)的promise等待這個200毫秒的promise摩疑。

注意: 正如剛才描述的,技術(shù)上講在這個交替中有兩個promise:一個200毫秒延遲的promise楷怒,和一個被第二個then(..)鏈接的promise鸠删。但你可能會發(fā)現(xiàn)將這兩個promise組合在一起更容易思考,因為Promise機(jī)制幫你把它們的狀態(tài)自動地混合到了一起烘贴。從這個角度講桨踪,你可以認(rèn)為return delay(200)創(chuàng)建了一個promise來取代早前一個返回的被鏈接的promise。

老實(shí)說纳账,沒有任何消息進(jìn)行傳遞的一系列延遲作為Promise流程控制的例子不是很有用疏虫。讓我們來看一個更加實(shí)在的場景:

與計時器不同呢袱,讓我們考慮發(fā)起Ajax請求:

// 假定一個`ajax( {url}, {callback} )`工具

// 帶有Promise的ajax
function request(url) {
    return new Promise( function(resolve,reject){
        // `ajax(..)`的回調(diào)應(yīng)當(dāng)是我們的promise的`resolve(..)`函數(shù)
        ajax( url, resolve );
    } );
}

我們首先定義一個request(..)工具,它構(gòu)建一個promise表示ajax(..)調(diào)用的完成:

request( "http://some.url.1/" )
.then( function(response1){
    return request( "http://some.url.2/?v=" + response1 );
} )
.then( function(response2){
    console.log( response2 );
} );

注意: 開發(fā)者們通常遭遇的一種情況是治专,他們想用本身不支持Promise的工具(就像這里的ajax(..),它期待一個回調(diào))進(jìn)行Promise式的異步流程控制喘批。雖然ES6原生的Promise機(jī)制不會自動幫我們解決這種模式饶深,但是在實(shí)踐中所有的Promise庫會幫我們這么做。它們通常稱這種處理為“提升(lifting)”或“promise化”或其他的什么名詞额湘。我們稍后再回頭討論這種技術(shù)锋华。

使用返回Promise的request(..),通過用第一個URL調(diào)用它我們在鏈條中隱式地創(chuàng)建了第一步纳猫,然后我們用第一個then(..)在返回的promise末尾進(jìn)行連接芜辕。

一旦response1返回倔丈,我們用它的值來構(gòu)建第二個URL需五,并且發(fā)起第二個request(..)調(diào)用。這第二個promisereturn的蜜氨,所以我們的異步流程控制的第三步將會等待這個Ajax調(diào)用完成记劝。最終定欧,一旦response2返回扩氢,我們就打印它录豺。

我們構(gòu)建的Promise鏈不僅是一個表達(dá)多步驟異步序列的流程控制双饥,它還扮演者將消息從一步傳遞到下一步的消息管道。

要是Promise鏈中的某一步出錯了會怎樣呢昏翰?一個錯誤/異常是基于每個Promise的棚菊,意味著在鏈條的任意一點(diǎn)捕獲這些錯誤是可能的检碗,而且這些捕獲操作在那一點(diǎn)上將鏈條“重置”后裸,使它回到正常的操作上來:

// 步驟 1:
request( "http://some.url.1/" )

// 步驟 2:
.then( function(response1){
    foo.bar(); // 沒有定義,錯誤因苹!

    // 永遠(yuǎn)不會跑到這里
    return request( "http://some.url.2/?v=" + response1 );
} )

// 步驟 3:
.then(
    function fulfilled(response2){
        // 永遠(yuǎn)不會跑到這里
    },
    // 拒絕處理器捕捉錯誤
    function rejected(err){
        console.log( err ); // 來自 `foo.bar()` 的 `TypeError` 錯誤
        return 42;
    }
)

// 步驟 4:
.then( function(msg){
    console.log( msg );     // 42
} );

當(dāng)錯誤在第2步中發(fā)生時,第3步的拒絕處理器將它捕獲款筑。拒絕處理器的返回值(在這個代碼段里是42)奈梳,如果有的話,將會完成下一步(第4步)的promise于宙,如此整個鏈條又回到完成的狀態(tài)捞魁。

注意: 就像我們剛才討論過的,當(dāng)我們從一個完成處理器中返回一個promise時旺上,它會被展開并有可能推遲下一步宣吱。這對從拒絕處理器中返回的promise也是成立的杭攻,這樣如果我們在第3步返回一個promise而不是return 42兆解,那么這個promise就可能會推遲第4步。不管是在then(..)的完成還是拒絕處理器中现拒,一個被拋出的異常都將導(dǎo)致下一個(鏈接著的)promise立即用這個異常拒絕印蔬。

如果你在一個promise上調(diào)用then(..),而且你只向它傳遞了一個完成處理器退唠,一個假定的拒絕處理器會取而代之:

var p = new Promise( function(resolve,reject){
    reject( "Oops" );
} );

var p2 = p.then(
    function fulfilled(){
        // 永遠(yuǎn)不會跑到這里
    }
    // 如果忽略或者傳入任何非函數(shù)的值,
    // 會有假定有一個這樣的拒絕處理器
    // function(err) {
    //     throw err;
    // }
);

如你所見寨蹋,這個假定的拒絕處理器僅僅簡單地重新拋出錯誤,它最終強(qiáng)制p2(鏈接著的promise)用同樣的錯誤進(jìn)行拒絕运褪。實(shí)質(zhì)上秸讹,它允許錯誤持續(xù)地在Promise鏈上傳播,直到遇到一個明確定義的拒絕處理器劣欢。

注意: 稍后我們會講到更多關(guān)于使用Promise進(jìn)行錯誤處理的細(xì)節(jié),因為會有更多微妙的細(xì)節(jié)需要關(guān)心校套。

如果沒有一個恰當(dāng)?shù)暮戏ǖ暮瘮?shù)作為then(..)的完成處理器參數(shù)笛匙,也會有一個默認(rèn)的處理器取而代之:

var p = Promise.resolve( 42 );

p.then(
    // 如果忽略或者傳入任何非函數(shù)的值,
    // 會有假定有一個這樣的完成處理器
    // function(v) {
    //     return v;
    // }
    null,
    function rejected(err){
        // 永遠(yuǎn)不會跑到這里
    }
);

如你所見涕蜂,默認(rèn)的完成處理器簡單地將它收到的任何值傳遞給下一步(Promise)机隙。

注意: then(null,function(err){ .. })這種模式——僅處理拒絕(如果發(fā)生的話)但讓成功通過——有一個縮寫的API:catch(function(err){ .. })。我們會在下一節(jié)中更全面地涵蓋catch(..)葱跋。

讓我們簡要地復(fù)習(xí)一下使鏈?zhǔn)搅鞒炭刂瞥蔀榭赡艿腜romise固有行為:

  • 在一個Promise上的then(..)調(diào)用會自動生成一個新的Promise并返回娱俺。
  • 在完成/拒絕處理器內(nèi)部,如果你返回一個值或拋出一個異常油宜,新返回的Promise(可以被鏈接的)將會相應(yīng)地被解析慎冤。
  • 如果完成或拒絕處理器返回一個Promise,它會被展開违寿,所以無論它被解析為什么值藤巢,這個值都將變成從當(dāng)前的then(..)返回的被鏈接的Promise的解析才沧。

雖然鏈?zhǔn)搅鞒炭刂坪苡杏梦略玻菍⑺J(rèn)為是Promise的組合方式的副作用可能最準(zhǔn)確,而不是它的主要意圖锅移。正如我們已經(jīng)詳細(xì)討論過許多次的非剃,Promise泛化了異步處理并且包裝了與時間相關(guān)的值和狀態(tài),這才是讓我們以這種有用的方式將它們鏈接在一起的原因鬓催。

當(dāng)然压怠,相對于我們在第二章中看到的一堆混亂的回調(diào),這種鏈條的順序表達(dá)是一個巨大的改進(jìn)布卡。但是仍然要蹚過相當(dāng)多的模板代碼(then(..) and function(){ .. })。在下一章中贸街,我們將看到一種極大美化順序流程控制的表達(dá)模式捐川,生成器(generators)古沥。

術(shù)語: Resolve(解析),F(xiàn)ulfill(完成)盹沈,和Reject(拒絕)

在你更多深入地學(xué)習(xí)Promise之前,在“解析(resolve)”歌亲,“完成(fulfill)”,和“拒絕(reject)”這些名詞之間還有一些我們需要辨明的小困惑悍缠。首先讓我們考慮一下Promise(..)構(gòu)造器:

var p = new Promise( function(X,Y){
    // X() 給 fulfillment(完成)
    // Y() 給 rejection(拒絕)
} );

如你所見,有兩個回調(diào)(標(biāo)識為XY)被提供了趴拧。第一個 通常 用于表示Promise完成了著榴,而第二個 總是 表示Promise拒絕了。但“通惩裕”是什么意思炼绘?它對這些參數(shù)的正確命名暗示著什么呢?

最終脚曾,這只是你的用戶代碼本讥,和將被引擎翻譯為沒有任何含義的東西的標(biāo)識符鲁冯,所以在 技術(shù)上 它無緊要拷沸;foo(..)bar(..)在功能性上是相等的。但是你用的詞不僅會影響你如何考慮這段代碼薯演,還會影響你所在團(tuán)隊的其他開發(fā)者如何考慮它撞芍。將精心策劃的異步代碼錯誤地考慮,幾乎可以說要比面條一般的回調(diào)還要差勁兒跨扮。

所以序无,某種意義上你如何稱呼它們很關(guān)鍵。

第二個參數(shù)很容易決定塘秦。幾乎所有的文獻(xiàn)都使用reject(..)做為它的名稱挨稿,因為這正是它(唯一3艏摇)要做的讳苦,對于命名來說這是一個很好的選擇束莫。我也強(qiáng)烈推薦你一直使用reject(..)逛绵。

但是關(guān)于第一個參數(shù)還是有些帶有歧義硕蛹,它在許多關(guān)于Promise的文獻(xiàn)中常被標(biāo)識為resolve(..)。這個詞明顯地是與“resolution(解析)”有關(guān),它在所有的文獻(xiàn)中(包括本書)廣泛用于描述給Promise設(shè)定一個最終的值/狀態(tài)梧疲。我們已經(jīng)使用“解析Promise(resolve the Promise)”許多次來意味Promise的完成(fulfilling)或拒絕(rejecting)韭畸。

但是如果這個參數(shù)看起來被用于特指Promise的完成甘萧,為什么我們不更準(zhǔn)確地叫它fulfill(..)卑硫,而是用resolve(..)呢风皿?要回答這個問題,讓我們看一下Promise的兩個API方法:

var fulfilledPr = Promise.resolve( 42 );

var rejectedPr = Promise.reject( "Oops" );

Promise.resolve(..)創(chuàng)建了一個Promise,它被解析為它被給予的值。在這個例子中,42是一個一般的损肛,非Promise不皆,非thenable的值,所以完成的promisefulfilledPr是為值42創(chuàng)建的凑队。Promise.reject("Oops")為了原因"Oops"創(chuàng)建的拒絕的promiserejectedPr

現(xiàn)在讓我們來解釋為什么如果“resolve”這個詞(正如Promise.resolve(..)里的)被明確用于一個既可能完成也可能拒絕的環(huán)境時,它沒有歧義择卦,反而更加準(zhǔn)確:

var rejectedTh = {
    then: function(resolved,rejected) {
        rejected( "Oops" );
    }
};

var rejectedPr = Promise.resolve( rejectedTh );

就像我們在本章前面討論的宁赤,Promise.resolve(..)將會直接返回收到的純粹的Promise漩勤,或者將收到的thenable展開。如果展開這個thenable之后是一個拒絕狀態(tài),那么從Promise.resolve(..)返回的Promise事實(shí)上是相同的拒絕狀態(tài)奏司。

所以對于這個API方法來說呻待,Promise.resolve(..)是一個好的炸茧,準(zhǔn)確的名稱,因為它實(shí)際上既可以得到完成的結(jié)果炫隶,也可以得到拒絕的結(jié)果。

Promise(..)構(gòu)造器的第一個回調(diào)參數(shù)既可以展開一個thenable(與Promise.resolve(..)相同)罩句,也可以展開一個Promise:

var rejectedPr = new Promise( function(resolve,reject){
    // 用一個被拒絕的promise來解析這個promise
    resolve( Promise.reject( "Oops" ) );
} );

rejectedPr.then(
    function fulfilled(){
        // 永遠(yuǎn)不會跑到這里
    },
    function rejected(err){
        console.log( err ); // "Oops"
    }
);

現(xiàn)在應(yīng)當(dāng)清楚了坡脐,對于Promise(..)構(gòu)造器的第一個參數(shù)來說resolve(..)是一個合適的名稱伶唯。

警告: 前面提到的reject(..) 不會resolve(..)那樣進(jìn)行展開希柿。如果你向reject(..)傳遞一個Promise/thenable值,這個沒有被碰過的值將作為拒絕的理由溜畅。一個后續(xù)的拒絕處理器將會受到你傳遞給reject(..)的實(shí)際的Promise/thenable,而不是它底層的立即值极祸。

現(xiàn)在讓我們將注意力轉(zhuǎn)向提供給then(..)的回調(diào)慈格。它們應(yīng)當(dāng)叫什么(在文獻(xiàn)和代碼中)?我的建議是fulfilled(..)rejected(..)

function fulfilled(msg) {
    console.log( msg );
}

function rejected(err) {
    console.error( err );
}

p.then(
    fulfilled,
    rejected
);

對于then(..)的第一個參數(shù)的情況遥金,它沒有歧義地總是完成狀態(tài)浴捆,所以沒有必要使用帶有雙重意義的“resolve”術(shù)語。另一方面稿械,ES6語言規(guī)范中使用onFulfilled(..)onRejected(..) 來標(biāo)識這兩個回調(diào)选泻,所以它們是準(zhǔn)確的術(shù)語。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市页眯,隨后出現(xiàn)的幾起案子梯捕,更是在濱河造成了極大的恐慌,老刑警劉巖窝撵,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件傀顾,死亡現(xiàn)場離奇詭異,居然都是意外死亡碌奉,警方通過查閱死者的電腦和手機(jī)短曾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赐劣,“玉大人错英,你說我怎么就攤上這事÷”” “怎么了椭岩?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長璃赡。 經(jīng)常有香客問我判哥,道長,這世上最難降的妖魔是什么碉考? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任塌计,我火速辦了婚禮,結(jié)果婚禮上侯谁,老公的妹妹穿的比我還像新娘锌仅。我一直安慰自己,他們只是感情好墙贱,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布热芹。 她就那樣靜靜地躺著,像睡著了一般惨撇。 火紅的嫁衣襯著肌膚如雪伊脓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天魁衙,我揣著相機(jī)與錄音报腔,去河邊找鬼。 笑死剖淀,一個胖子當(dāng)著我的面吹牛纯蛾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播纵隔,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼翻诉,長吁一口氣:“原來是場噩夢啊……” “哼帆卓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起米丘,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤剑令,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拄查,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吁津,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年堕扶,在試婚紗的時候發(fā)現(xiàn)自己被綠了碍脏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡稍算,死狀恐怖典尾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情糊探,我是刑警寧澤钾埂,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站科平,受9級特大地震影響褥紫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瞪慧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一髓考、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧弃酌,春花似錦氨菇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至多柑,卻和暖如春奶是,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背竣灌。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留秆麸,地道東北人初嘹。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像沮趣,于是被迫代替她去往敵國和親屯烦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評論 2 353

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