摘要:一個就像一個樂高玩具。問題是不是你小時候玩兒的那個有趣,它們不是充滿想象力的打氣筒,也不是一種樂高玩具。這是對的并不是給開發者使用的,它們是給庫作者使用的。不會超過這兩種情況。第二個是根據第一個處理函數如何運行來自動變成狀態成功或者失敗。
原文地址:http://blog.getify.com/promis...
在 Part4:擴展問題 中,我討論了如何擴展和抽象Promise是多么的常見,以及這中間的一些問題。但是為什么promise對于開發者來說不是足夠友好的呢?這就是它的設計用意嗎?
I"ve Got Friends In Low PlacesPromise被設計為低級別的構建塊。一個promise就像一個樂高玩具。單個樂高只是一個有趣的玩具。但是如果把它們拼在一起,你會感受到更多的樂趣。
問題是promise不是你小時候玩兒的那個有趣LEGO,它們不是充滿想象力的打氣筒,也不是Gandalf mini-figure(一種樂高玩具)。
都不是,promise只是你的簡單老舊的4X2的磚塊。
這并不是使它們非常有用。但是它們是你箱子中最重要的組成部分之一。當它們彼此分開時它們只是這么個東西,但是當把它們整合在一起它們就會散發出光芒。
換句話說,promise本質上是一個構建在真實用戶之上的低級別的API。這是對的:promise并不是給開發者使用的,它們是給庫作者使用的。
你會從它們那收益許多,但是你很可能不是直接使用它們。你將會使用的是經過許多庫組合包裝之后的結果。
控制 VS 值請允許我矯正第一個最大的關于promise的誤解:它們不是真正關于流程控制的。
promise當然可以鏈接在一起來變成近似異步流程控制的東西。但是最后證明它們并不像你想象的那樣擅長這個任務。promises確實只是一個值的容器。這個值可能現在就存在也可能是未來的一個值。但是不管怎樣,它只是一個值。這是promise最有意義的好處之一。它們在值的上面創建了一個強大的抽象使得值不再是暫存的東西。換句話說,不管那個值現在是否存在,你都可以用同樣的方式使用promise。在這個系列的 第三部分 中,我討論過promise必須是不可變的,它們作為值的意義也是基于這個特點的。
promises就像狀態的小型的自包含的表現方式。它們是可組合的,也就意味著你全部的程序可以用它們來表示。
限制就像你不能奢望一個多帶帶的4X2的樂高可以變成一個跑車,讓promise成為你的異步流程控制機制也是一種奢望。
那么promises作為一個非暫存的不可變的值對于解決異步任務意味著什么呢?在它們設計哲學的約束中,有它們擅長并且可以有幫助的東西。
在剩下的內容中,我會討論這個限制。但是我并不打算作為一個promise的批判者。我試圖去強調擴展和抽象的重要性。
錯誤處理當我說promise只是一個值的容器的時候我撒了個小慌。實際上,它是一個成功值或者失敗信息的容器。在任何時候,一個promise是一個未來的成功值或者在獲取這個值時的失敗信息。不會超過這兩種情況。
在某種意義上說,一個promise是一個決策結構,一個if..then..else。其他人喜歡把它想成一個try..catch結構。不管是哪種理解,你就像在說"請求一個值,不管成功還是失敗"。
就像尤達說,"Do or do not, there is no try"。
考慮下面這個情況:
function ajax(url) { return new Promise( function(resolve,reject){ // make some ajax request // if you get a response, `resolve( answer )` // if it fails, `reject( excuses )` } ); } ajax( "http://TheMeaningOfLife.com" ) .then( winAtLife, keepSearching );
看到winAtLife()和keepSearching()函數了嗎?我們在說,"去問問生命的意義,不管你有沒有找到答案,我們都繼續"。
如果我們不傳入keepSearching會怎樣?除了作為一個樂觀主義者假設你會找到答案然后在生命長河中取勝,這里會有什么危險呢?
如果promise沒有找到生命的意義(或者如果在處理答案的過程中發生了javascript異常),它會默默地保留著錯誤的事實,也許會永遠保留著。就算你等上一百萬年,你都不會知道對于答案的請求失敗了。
你只能通過觀察才能知道它失敗了。這可能需要深入到形而上學或者量子學的東西。讓我們停止在這吧。
所以不帶失敗處理函數的promise是一個會默默地失敗的promise。這并不好。這意味著如果你忘記了,你會陷入失敗的陷阱而不是成功。
所以你會懷疑:為什么promises會忽略失敗處理函數呢?因為你可能現在不在意失敗的情況,只有以后某個時刻會關心。我們程序的暫時性意味著系統現在不會知道你以后會想做什么。現在忽略失敗處理函數也許對你來說是正合適的,因為你知道你會把這個promise鏈接到另一個promise,并且那個promise有一個失敗處理函數。
所以promise機制讓你可以創建不需要監聽失敗的promise。
這里有一個很微妙的問題,很可能也是大多數剛接觸promise的開發者會碰到的問題。
束縛我們的鏈子為了理解這個問題,我們首先需要理解promises是如何鏈接在一起的。我認為你會很快明白promise鏈是強大并且有一點復雜的。
ajax( "http://TheMeaningOfLife.com" ) .then( winAtLife, keepSearching ) // a second promise returned here that we ignored! ;
ajax(..)調用產生了第一個promise,然后then(..)調用產生了第二個promise。我們沒有捕捉并且觀察在這段代碼中的第二個promise,但是我們可以。第二個promise是根據第一個promise處理函數如何運行來自動變成fulfilled狀態(成功或者失敗)。
第二個promise不會在意第一個promise是成功還是失敗。它在意第一個promise的處理函數(不管成功還是失敗)。
這是promise鏈的關鍵。但是這有一點不好理解,所以重復讀上面那段話直到你理解為止。
考慮下promise代碼通常是怎么寫的(通過鏈):
ajax( ".." ) .then( transformResult ) .then( displayAnswer, reportError );
這段代碼也可以像下面這么寫,效果是一樣的:
var promiseA = ajax( ".." ); var promiseB = promiseA.then( transformResult ); var promiseC = promiseB.then( displayAnswer, reportError ); // we don"t use `promiseC` here, but we could...
Promise A是唯一在意ajax(..)結果的promise。
Promise B只關心Promise A在transformResult(..)函數內部是如何處理的(不是Promise A的結果本身),同樣的,Promise C只關心Promise B在displayAnswer(..)或者reportError(..)函數內部是如何處理的(不是Promise B結果本身)。
再一次,重復讀這段話直到理解。
在transformResult(..)內部,如果它立刻完成了它的任務,然后Promise B就會立刻完成,不管成功還是失敗。然而,如果transformResult(..)不能立刻完成,而是創建它自己的promise,我們稱它為Promise H1("H"是"hidden",因為它是隱藏在內部的)。原本Promise B返回的等待我們如何處理Promise A的promise,現在概念上被Promise H1替換了(并不是真的替換了,只是被說成一樣的)。
所以,現在當你說promiseB.then(..)時,它實際上就像說promiseH1.then(..)。如果Promise H1成功了,displayAnswer(..)會被調用,但是如果它失敗了,reportError(..)會被調用。
這就是promise鏈是如何工作的。
但是,如果Promise A(由ajax調用返回)失敗了會怎樣?promiseA.then(..)調用沒有注冊一個失敗處理函數。它會默默地隱藏錯誤嗎?它會的,除了我們鏈接上Promise B然后在上面注冊一個錯誤處理函數:reportError(..)。如果Promise A失敗了,transformResult(..)不會被調用,并且沒有錯誤處理函數,所以Promise B馬上被標記為失敗,所以reportError(..)會被調用。
如果Promise A成功了,transformResult(..)會被執行,然后當運行transformResult(..)時有一個錯誤會怎樣?Promise B被標記為失敗,然后reportError(..)也會被調用。
但是這里是危險的地方,這個地方甚至有經驗的開發者都會遺漏的!
如果Promise A成功了(成功的ajax(..)),然后Promise B成功了(成功的transformResult(..)),但是當運行displayAnswer(..)時有一個錯誤會怎樣?
你也許會認為reportError(..)會被調用?大多數人會這么想,但是不是的。
為什么?因為來自displayAnswer(..)的一個錯誤或者失敗promise導致一個失敗的Promise C。我們監聽Promise C失敗的情況了嗎?仔細看看。沒有。
為了確保你不會漏掉這種錯誤并且讓它默默地隱藏在Promise C狀態內部,你也會希望監聽Promise C的失敗:
var promiseC = promiseB.then( displayAnswer, reportError ); // need to do this: promiseC.then( null, reportError ); // or this:, which is the same thing: promiseC.catch( reportError ); // Note: a silently ignored *Promise D* was created here!
OK,所以現在我們捕獲displayAnswer(..)內部的錯誤。不得不去記住這個有一點坑爹。
烏龜但是有一個更加微妙的問題!如果當處理displayAnswer(..)返回的錯誤時,reportError(..)函數也有一個JS異常會怎樣?會有人捕獲這個錯誤嗎?沒有。
看!上面有一個隱含的Promise D,并且它會被告知reportError(..)內部的異常。
OMG,你肯定會想。什么時候才能停止?它會這樣一直下去嗎?
一些promise庫作者認為有必要解決這個問題通過讓"安靜的錯誤"被作為全局異常拋出。但是這種機制該如何得知你不想再鏈接promise并且提供一個錯誤處理函數呢?它如何知道什么時候應該通報一個全局異常或者不通報呢?你肯定不希望當你已經捕獲并且處理錯誤的情況下仍然有很多控制臺錯誤信息。
在某種意義上,你需要可以標記一個promise為“final”,就像說“這是我鏈子中的最后一個promise”或者“我不打算再鏈接了,所以這是烏龜停止的地方”。如果在鏈的最后發生了錯誤并且沒有被捕獲,然后它需要被報告為一個全局異常。
從表面上我猜測這似乎是很明智的。這種情況下的實現像下面這樣:
var promiseC = promiseB.then( displayAnswer, reportError ); promiseC .catch( reportError ) .done(); // marking the end of the chain
你仍然需要記住調用done(),要不然錯誤還是會隱藏在最后一個promsie中。你必須使用穩固的錯誤處理函數。
"惡心",你肯定會這么想。歡迎來到promises的歡樂世界。
對于錯誤處理已經說了很多了。另一個核心promsie的限制是一個promise代表一個多帶帶的值。什么是一個多帶帶的值呢?它是一個對象或者一個數組或者一個字符串或者一個數字。等等,我還可以在一個容器里放入多個值,就像一個數組或對象中的多個元素。Cool!
一個操作的最終結果不總是一個值,但是promise并不會這樣,這很微妙并且又是另一個失敗陷阱:
function ajax(url) { return new Promise( function(resolve,reject){ // make some ajax request // if you get a response, `resolve( answer, url )` // if it fails, `reject( excuses, url )` } ); } ajax( ".." ) .then( function(answer,url){ console.log( answer, url ); // .. undefined }, function(excuses,url){ console.log( excuses, url ); // .. undefined } );
你看出這里面的問題了嗎?如果你意外的嘗試傳遞超過一個的值過去,不管傳給失敗處理函數還是成功處理函數,只有第一個值能被傳遞過去,其他幾個會被默默地丟掉。
為什么?我相信這和組合的可預測性有關,或者一些其他花哨的詞匯有關。最后,你不得不記住包裹自己的多個值要不然你就會不知不覺的丟失數據。
并行真實世界中的app經常在“同一時間”發生超過一件事情。本質上說,我們需要構建一個處理器,并行處理多個事件,等待它們全部完成再執行回調函數。
相比于promise問題,這是一個異步流程控制的問題。一個多帶帶的promise不能表達兩個或更多并行發生的異步事件。你需要一個抽象層來處理它。
在計算機科學術語中,這個概念叫做一個“門”。一個等待所有任務完成,并且不關心它們完成順序的門。
在promise世界中,我們添加一個API叫做Promise.all(..),它可以構建一個promise來等待所有傳遞進來的promise完成。
Promise.all([ // these will all proceed "in parallel" makePromise1(), makePromise2(), makePromise3() ]) .then( .. );
一個相近的方法是race()。它的作用和all()一樣,除了它只要有一個promise返回消息就執行回調函數,而不等待其他promise的結果。
當你思考這些方法的時候,你可能會想到許多方式來實現這些方法。Promise.all(..)和Promise.race(..)是原生提供的,因為這兩個方法是很常用到的,但是如果你還需要其他的功能那么你就需要一個庫來幫助你了。限制的另一個表現就是你很快就會發現你需要自己使用Array的相關方法來管理promise列表,比如.map(..)和.reduce(..)。如果你對map/reduce不熟悉,那么趕緊去熟悉一下,因為你會發現當處理現實世界中promise的時候你經常會需要它們。
幸運的是,已經有很多庫來幫助你了,并且每天還有很多新的庫被創造出來。
Single Shot Of Espresso,Please!另一個關于promise的事情是它們只會運行一次,然后就不用了。
如果你只需要處理單個事件,比如初始化一個也沒或者資源加載,那么這樣沒什么問題。但是如果你有一個重復的事件(比如用戶點擊按鈕),你每次都需要執行一系列異步操作會怎么樣呢?Promise并不提供這樣的功能,因為它們是不可變的,也就是不能被重置。要重復同樣的promise,唯一的方法就是重新定義一個promise。
$("#my_button").click(function(evt){ doTask1( evt.target ) .then( doTask2 ) .then( doTask3 ) .catch( handleError ); });
太惡心了,不僅僅是因為重復創建promise對于效率有影響,而且它對于職責分散不利。你不得不把多個事件監聽函數放在同一個函數中。如果有一個方式來改變這種情況就好了,這樣事件監聽和事件處理函數就能夠分開了。
Microsoft的RxJS庫把這種方式叫做"觀察者模式"。我的asynquence庫有一個react(..)方法通過簡單的方式提供了一個類似的功能。
盲區...在一個已經被使用回調函數的API占據的世界中,把promise插入到代碼中比我們想象的要困難。考慮下面這段代碼:
function myAjax(url) { return new Promise( function(resolve,reject){ ajax( url, function(err,response){ if (err) { reject( err ); } else { resolve( response ); } } ) } ); }
我認為promise解決了回調地獄的問題,但是它們代碼看起來仍然像垃圾。我們需要抽象層來使得用promise表示回調變得更簡單。原生的promise并沒有提供這個抽象層,所以結果就是通過原生promise寫出來的代碼還是很丑陋。但是如果有抽象層那么事情就變得很簡單了。
例如,我的asynquence庫提供了一個errfcb()插件(error-first callback),用它可以構建一個回調來處理下面這種場景:
function myAjax(url) { var sq = ASQ(); ajax( url, sq.errfcb() ); return sq; }Stop The Presses!
有時,你想要取消一個promise而去做別的事情,但是如果現在你的promise正處在掛起狀態會怎樣呢?
var pr = ajax( ".." ) .then( transformResult ) .then( displayAnswer, reportError ); // Later pr.cancel(); // <-- doesn"t work!
所以,為了取消promise,你需要引入一下東西:
function transformResult(data) { if (!pr.ignored) { // do something! } } var pr = ajax( ".." ) .then( transformResult ) .then( displayAnswer, reportError ); // Later pr.ignored = true; // just hacking around
換句話說,你為了能夠取消你的promise,在promise上面加了一層來處理這種情況。你不能從promise取消注冊處理函數。并且因為一個promise必須不可變,你能夠直接取消一個promise這種情況是不允許出現的。從外部取消一個promise跟改變它的狀態沒有什么區別。它使得promise變得不可靠。
許多promise庫都提供了這種功能,但是這明顯是一個錯誤。取消這種行為是不需要promise,但是它可以出現在promise上面的一個抽象層里。
冗長另一個關于原生promise的擔心是有些事情并沒有被實現,所以你必須自動手動實現它們,而這些事情對于可擴展性是很重要的,但是這些東西經常會導致令人討厭的重復代碼。
看一個例子,在每一個promise的完成步驟中,有一個設定就是你希望保持鏈式結構,所以then(..)方法會返回一個新的promise。但是如果你想要加入一個自己創建的promise并且從一個成功處理函數中返回,這樣你的promise就可以加入到鏈的流程控制中。
function transformResult(data) { // we have to manually create and return a promise here return new Promise( function(resolve,reject){ // whatever } ); } var pr = ajax( ".." ) .then( transformResult ) .then( displayAnswer, reportError );
不同的是,就像上面解釋的一樣,從第一個then(..)返回的隱藏的promise立刻就完成(或者失敗),然后你就沒辦法讓剩下的鏈異步延遲。如果有一個抽象層能夠通過某種方式把自動創建/鏈接的promise暴露給你,然后你就不需要創建自己的promise來替換了,這樣該多好。
換句話說,如果有一個設定假設你需要為了異步的目的使用鏈,而不是你只是需要漂亮得執行異步。(也就是說你確實是希望你的代碼可以異步執行,而不是說希望整個異步流程看過去好看點)。
另一個例子:你不能直接傳遞一個已經存在的promise給then(..)方法,你必須傳遞一個返回這個promise的函數。
var pr = doTask2(); doTask1() .then( pr ); // would be nice, but doesn"t work! // instead: doTask1() .then( function(){ return pr; } );
這個限制性是有很多原因的。但是它只是減弱了有利于保持可擴展性和可預測性的用法的簡潔。抽象可以容易的解決這個問題。
全劇終所有這些原因就是為什么原生的promise API是強大同時也是有局限性的。
關于擴展和抽象是一個成熟的領域。許多庫正在做這些工作。就像我之前說的,asynquence是我自己的promise抽象庫。它很小但是很強大。它解決了所有博客中提到的promise的問題。
我后面會寫一篇詳細的博客來介紹asynquence是如果解決這些問題的,所以敬請期待。
深入理解Promise五部曲--1.異步問題
深入理解Promise五部曲--2.轉換問題
深入理解Promise五部曲--3.可靠性問題
深入理解Promise五部曲--4.擴展性問題
深入理解Promise五部曲--5.樂高問題
最后,安利下我的個人博客,歡迎訪問:http://bin-playground.top
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/87598.html
摘要:當引擎開始執行一個函數比如回調函數時,它就會把這個函數執行完,也就是說只有執行完這段代碼才會繼續執行后面的代碼。當條件允許時,回調函數就會被運行。現在,返回去執行注冊的那個回調函數。 原文地址:http://blog.getify.com/promis... 在微博上看到有人分享LabJS作者寫的關于Promise的博客,看了下覺得寫得很好,分五個部分講解了Promise的來龍去脈。從...
摘要:有一個和相關的更大的問題。最后,請負有責任感并且使用安全的擴展。深入理解五部曲異步問題深入理解五部曲轉換問題深入理解五部曲可靠性問題深入理解五部曲擴展性問題深入理解五部曲樂高問題最后,安利下我的個人博客,歡迎訪問 原文地址:http://blog.getify.com/promis... 現在,我希望你已經看過深入理解Promise的前三篇文章了。并且假設你已經完全理解Promises...
摘要:只要在調用異步函數時設置一個或多個回調函數,函數就會在完成時自動調用回調函數。要解決的問題是,如何將回調方法的參數從回調方法中傳遞出來,讓它可以像同步函數的返回結果一樣,在回調函數以外的控制范圍內,可以傳遞和復用。 摘要: 我們知道 JavaScript 自從有了 Generator 之后,就有了各種基于 Generator 封裝的協程。其中 hprose 中封裝的 Promise 和...
摘要:直到最近,我們仍然在用簡單的回調函數來處理異步的問題。當我們只有一個異步任務的時候使用回調函數看起來還不會有什么問題。 原文地址:http://blog.getify.com/promis... 廈門旅行歸來,繼續理解Promise 在上一篇深入理解Promise五部曲:1.異步問題中,我們揭示了JS的異步事件輪詢并發模型并且解釋了多任務是如何相互穿插使得它們看起來像是同時運行的。...
摘要:簡單的說,即將到來的標準指出是一個,所以作為一個,必須可以被子類化。保護還是子類化這是個問題我真的希望我能創建一個忠實的給及以下。 原文地址:http://blog.getify.com/promis... 如果你需要趕上我們關于Promise的進度,可以看看這個系列前兩篇文章深入理解Promise五部曲--1.異步問題和深入理解Promise五部曲--2.控制權轉移問題。 Promi...
閱讀 1860·2021-11-22 15:24
閱讀 1316·2021-11-12 10:36
閱讀 3219·2021-09-28 09:36
閱讀 1845·2021-09-02 15:15
閱讀 2760·2019-08-30 15:54
閱讀 2400·2019-08-30 11:02
閱讀 2398·2019-08-29 13:52
閱讀 3548·2019-08-26 11:53