[譯] 深刻理解 Promise 五部曲:2. 控制權轉換問題

原文地址:http://blog.getify.com/promis...javascript

廈門旅行歸來,繼續理解Promisejava

在上一篇深刻理解Promise五部曲:1.異步問題中,咱們揭示了JS的異步事件輪詢併發模型而且解釋了多任務是如何相互穿插使得它們看起來像是同時運行的。而後咱們討論了爲何咱們努力地在咱們的代碼裏表達這些東西以及爲何咱們的大腦不善於理解它們。segmentfault

咱們如今要找出一個更好的方式來表達異步流程,而後看看Promises是怎麼解決這個問題的。promise

回調嵌套

JS從一開始就使用事件輪詢的併發模型。咱們一直以來都在寫異步的程序。直到最近,咱們仍然在用簡單的回調函數來處理異步的問題。併發

makeAjaxRequest(url,function(respnose){
    alert("Response:" + response) ;
}) ;

當咱們只有一個異步任務的時候使用回調函數看起來還不會有什麼問題。可是,實際是咱們完成一個任務一般須要多個異步操做。例如:app

btn.addEventListener("click",function(evt){
    makeAjaxRequest(url,function(response){
        makeAjaxRequest(anotherURL + "?resp=" + response,function(response2){
            alert("Response2:" + response) ;
        })
    }) ;
},false) ;

把一系列異步操做連接在一塊兒最天然的方式就是使用回調嵌套,步驟2嵌套在步驟1中而後步驟3嵌套在步驟2中,等等。異步

回調地獄

你使用越多的回調,就會有越多的嵌套,不斷縮進意大利麪條似的代碼。很顯然,這種代碼難以編寫,難以理解並且難以維護。若是咱們花點時間來理清這些代碼每每會讓咱們事半功倍。這類嵌套/縮進常常被叫作"回調地獄"。有時也被叫作"回調金字塔",專指因爲代碼不斷縮進所造成的金字塔形狀,縮進越多金字塔形狀越明顯。編輯器

可是我仍是以爲"回調地獄"真的跟嵌套和縮進扯不上太大的關係。若是以前有人跟你說回調地獄就是指嵌套和縮進的話,不要相信他,由於他們並不理解回調真正的問題在哪兒。函數

可靠性缺失

回調(不管是否有嵌套)的真正問題是遠比編輯器中的空白符嚴重。讓咱們來分析下下面這個簡單的回調發生了什麼網站

//1.everything in my program before now

someAsyncThing(function(){
    //2.everything in my program for later
}) ;

你看清這段代碼說了什麼嗎?你從根本上把你的程序分紅了兩個部分:

  1. 直到如今爲止發生的事情

  2. 之後會發生的事情

換句話說,你把第二部分代碼包裝在一個回調函數中而後延遲到後面執行。

可是這並非問題,真正問題是在1和2之間發生了什麼。請問在這段時間內是誰在控制這些。
someAsyncThing(..)控制着這些。是你本身擁有並管理someAsyncThing()嗎?許多時候不是。更重要的是,你有多信任someAsyncThing(..)?你會問,信任什麼?無論你意識到沒有,你潛在的相信someAsyncThing(..)會作到下面這些:

  1. 不會太早調用個人回調函數

  2. 不會太遲調用個人回調函數(1,2就是說會在適當的時候調用回調函數)

  3. 不會調用個人回調太少次(不會少於實際應該調用的次數,好比不會漏掉函數調用)

  4. 不會調用個人回調太屢次(不會多於實際應該調用的次數,好比重複調用)

  5. 會給個人回調提供必要的參數

  6. 在個人回調失敗的時候會提醒我

咳!你也太信任它了!

實際上,這裏真正的問題是因爲回調引發的控制轉移。在你的程序的前半部分,你控制着程序的進程。如今你轉移了控制權,someAsyncThing(..)控制了你剩餘程序何時返回以及是否返回。控制轉移代表了你的代碼和其餘人的代碼之間的過分信任關係。

恐嚇戰術

someAsyncThing(..)是第三方庫的一個方法而且你沒法控制不能檢查的時候會發生什麼?只能祝你好運了!

好比你有一個電子商務網站,用戶就要完成付款的步驟了,可是在扣費以前有最後一個步驟,它須要通知一個第三方跟蹤庫。你調用他們API,而且提供一個回調函數。大部分狀況下,這不會有什麼問題。可是,在此次業務中,有一些你和他們都沒有意識到的奇怪的Bug,結果就是第三方庫在超時以前五秒的時間內每隔一秒就會調用一次回調函數。猜猜發生了什麼?在這個回調裏調用了chargeTheCreditCard()

Oops,消費者被扣了五次錢。爲何?由於你相信第三方庫只會調用你的回調一次。

因此你不得不被丟雞蛋而且給消費者道歉歸還多扣的四次錢。而後你馬上採起措施確保這種狀況不會再發生。你會怎麼作呢?

你可能會建立一些狀態值來跟蹤你的回調,當它被調用一次以後會被標記,而後就能夠忽略任何意外的重複調用。不管第三方如何道歉而且承諾他們的bug已經修復了,你不再會相信他們了,不是嗎?

這看起來像一個愚蠢的場景,可是這可能比你想得還廣泛。咱們的程序變得越複雜,咱們就會集成越多的第三方/外部代碼,這種愚蠢的場景就越容易發生。

布基膠帶

你給你的回調加入了狀態跟蹤機制,而後睡了一個好覺。可是實際上你只是處理了信任列表許多項目中的一項。當另外一個bug形成另外一個可靠性丟失的狀況時會發生什麼?更多的改造,更多醜陋的代碼。

更多布基膠帶。你必須不斷修復回調中的漏洞。不管你是多優秀的開發者,不管你的布基膠帶多漂亮,事實就是:在你信任牆上的回調充滿了漏洞。

Promise解決方案

一些人喜歡使用布基繃帶而且給信任牆上的洞打補丁。可是在某些時候,你也許會問本身,是否有其餘模式來表達異步流程控制,不須要忍受全部這些可靠性丟失?

是的!Promises就是一個方法。

在我解釋它們是怎麼工做以前,讓我來解釋一些它們背後的概念問題。

快餐業務

你走進你最喜好的快餐店,走到前臺要了一些美味的食物。收銀員告訴你一共7.53美圓而後你把錢給她。她會給回你什麼東西呢?

若是你足夠幸運,你要的食物已經準備好了。可是大多數狀況下,你會拿到一個寫着序列號的小票,是吧?因此你站到一邊等待你的食物。

很快,你聽到廣播響起:「請317號取餐」。正好是你的號碼。你走到前臺用小票換來你的食物!謝天謝地,你不用忍受太長的等待。

剛纔發生的是一個對於Promises很好的比喻。你走到前臺開始一個業務,可是這個業務不能立刻完成。因此,你獲得一個在遲些時候完成業務(你的食物)的promise(小票)。一旦你的食物準備就緒,你會獲得通知而後你第一時間用你的promise(小票)換來了你想要的東西:食物。

換句話說,帶有序列號的小票就是對於一個將來結果的承諾。

完成事件

想一想上面調用someAsyncThing(..)的例子。若是你能夠調用它而後訂閱一個事件,當這個調用完成的時候你會獲得通知而不是傳遞一個回調給它,這樣難道不會更好嗎?

例如,想象這樣的代碼:

var listener = someAsyncThing(..) ;
listener.on("completion",function(data){
    //keep going now !
}) ;

實際上,若是咱們還能夠監聽調用失敗的事件那就更好了。

listener.on("failure",function(){
    //Oops,What's plan B?
}) ;

如今,對於咱們調用的每一個函數,咱們可以在函數成功執行或者失敗的時候獲得通知。換句話說,每一個函數調用會是流程控制圖上的決策點。

Promise"事件"

Promises就像是一個函數在說「我這有一個事件監聽器,當我完成或者失敗的時候會被通知到。」咱們看看它是怎麼工做的:

function someAsyncThing(){
    var p = new Promise(function(resolve,reject){
        //at some later time,call 'resolve()' or 'reject()'
    }) ;
    return p ;
}
var p = someAsyncThing() ;
p.then(
    function(){
        //success happened    
    },
    function(){
        //failure happened
    }
) ;

你只須要監聽then事件,而後經過知道哪一個回調函數被調用就能夠知道是成功仍是失敗。

逆轉

經過promises,咱們從新得到了程序的控制權而不是經過給第三方庫傳遞迴調來轉移控制權。這是javascript中異步控制流程表達上一個很大的進步。

「等等」,你說。「我仍然要傳遞迴調啊。有什麼不同?!」嗯。。。好眼力!

有些人聲稱Promises經過移除回調來解決「回調地獄」的問題。並非這樣!在一些狀況下,你甚至須要比之前更多的回調。同時,根據你如何編寫你的代碼,你可能仍然須要把promises嵌套在別的promises中!

批判性地看,promises所作的只是改變了你傳遞迴調的地方。

本質上,若是你把你的回調傳遞給擁有良好保證和可預測性的中立Promises機制,你實質上從新得到了對於後續程序能很穩定而且運行良好的可靠性。標準的promises機制有如下這些保證:

  1. 若是promise被resolve,它要不是success就是failure,不可能同時存在。

  2. 一旦promise被resolve,它就不再會被resolve(不會出現重複調用)。

  3. 若是promise返回了成功的信息,那麼你綁定在成功事件上的回調會獲得這個消息。

  4. 若是發生了錯誤,promise會收到一個帶有錯誤信息的錯誤通知。

  5. 不管promise最後的結果是什麼(success或者failure),他就不會改變了,你老是能夠得到這個消息只要你不銷燬promise。

若是咱們從someAsyncThing(..)獲得的promise不是可用的標準的promise會發生什麼?若是咱們沒法判斷咱們是否可相信它是真的promise會怎麼樣?

簡單!只要你獲得的是「類promise」的,也就是擁有then(..)方法能夠註冊success和failure事件,那麼你就可用使用這個「類promise」而後把它包裝在一個你信任的promise中。

var notSureWhatItIs = someAsyncThing();

var p = Promise.resolve( notSureWhatItIs );

// now we can trust `p`!!
p.then(
    function(){
        // success happened 
    },
    function(){
        // failure happened 
    }
);

promises的最重要的特色就是它把咱們處理任何函數調用的成功或者失敗的方式規範成了可預測的形式,特別是若是這個調用實際上的異步的。

在這個規範過程當中,它使咱們的程序在可控制的位置而不是把控制權交給一個不可相信的第三方。

總結

不要管你所聽到的,「回調地獄」不是真的關於函數嵌套和它們在代碼編輯器中產生的縮進。它是關於控制轉移的,是指咱們因爲把控制權交給一個咱們不能信任的第三方而產生的對咱們的程序失去控制的現象。

Promises逆轉了這個狀況,它使得咱們從新得到控制權。相比傳遞迴調給第三方函數,函數返回一個promise對象,咱們可使用它來監聽函數的成功或失敗。在promise咱們仍然使用回調,可是重要的是標準的promise機制使咱們能夠信任它們行爲的正確性。咱們不須要想辦法來處理這些可靠性問題。

在第三部分:可靠性問題中,我會說道一個promises可靠性機制中很特別的部分:一個promise的狀態必須是可靠而且不可變的。

深刻理解Promise五部曲--1.異步問題
深刻理解Promise五部曲--2.轉換問題
深刻理解Promise五部曲--3.可靠性問題
深刻理解Promise五部曲--4.擴展性問題
深刻理解Promise五部曲--5.樂高問題

最後,安利下個人我的博客,歡迎訪問:http://bin-playground.top

相關文章
相關標籤/搜索