AngularJS 中的 Promise 和 設計模式

其實在 Javascript 中,有另外一種異步處理模式:更屌,在 Javascript 里面經常被叫做Promises, CommonJS 標準委員會于是發布了一個規范,就把這個 API 叫做Promises了。

Promise 背后的概念非常簡單,有兩部分:

1、Deferreds,定義工作單元

2、Promises,從 Deferreds 返回的數據

基本上,你會用 Deferred 作為通信對象,用來定義工作單元的開始,處理和結束三部分。

Promise 是 Deferred 響應數據的輸出;它有狀態 (等待,執行和拒絕),以及句柄,或叫做回調函數,反正就是那些在 Promise 執行,拒絕或者提示進程中會被調用的方法。

Promise 不同于回調的很重要的一個點是,你可以在 Promise 狀態變成執行(resolved)追加處理句柄。這就允許你傳輸數據,而忽略它是否已經被應用獲取,然后緩存它,等等之類的操作,因此你可以對數據執行操作,而不管它是否已經或者即將可用。

在之后的文章中,我們將會基于 AngularJS 來講解 Promises 。AngularJS 的整個代碼庫很大程度上依賴于 Promise,包括框架以及你用它編寫的應用代碼。AngularJS 用的是它自己的 Promises 實現,$q服務,又一個 Q 庫的輕量實現。

$q實現了上面提到的所有 Deferred / Promise 方法,除此之外$q還有自己的實現:$q.defer(),用來創建一個新的 Deferred 對象;$q.all(),允許等待多 Promises 執行終了,還有方法$q.when()和$q.reject(),具體我們之后會講到。

$q.defer()返回一個 Deferred 對象,帶有方法resolve(),reject(), 和notify()。Deferred 還有一個promise屬性,這是一個promise對象,可以用于應用內部傳遞。

promise 對象有另外三個方法:.then(),是唯一 Promise 規范要求的方法,用三個回調方法作為參數;一個成功回調,一個失敗回調,還有一個狀態變化回調。

$q在 Promise 規范之上還添加了兩個方法:catch(),可以用于定義一個通用方法,它會在 promise 鏈中有某個 promise 處理失敗時被調用。還有finally(),不管 promise 執行是成功或者失敗都會執行。注意,這些不應該和 Javascript 的異常處理混淆或者并用: 在 promise 內部拋出的異常,不會被catch()俘獲。(※貌似這里我理解錯了)

Promise 簡單例子

下面是使用$q,Deferred,和Promise放一起的簡單例子。首先我要聲明,本文中所有例子的代碼都沒有經過測試;而且也沒有正確的引用Angular服務和依賴,之類的。不過我覺得對于啟發你怎么玩,已經夠好了。

首先,我們先創建一個新的工作單元,通過 Deferred 對象,用$q.defer():

然后,我們從 Deferred 拿到promise,給它追加一些行為。

最后,我們假裝做點啥,然后告訴 deferred 我們已經完成了:

當然,這不需要真的異步,所以我們可以用 Angular 的$timeout服務(或者 Javascript 的setTimeout,不過,在 Angular 應用中最好用$timeout,這樣你可以 mock/test 它)來假裝一下。

好了,有趣的是:我們可以追加很多個then()到一個 promise 上,以及我們可以在 promise 被 resolved 之后追加then():

那,要是發生異常怎么辦?我們用deferred.reject(),它會出發then()的第二個函數,就像回調一樣。

不用then()的第二個參數,還有另外一種選擇,你可以用鏈式的catch(),在 promise 鏈中發生異常的時候它會被調用(可能在很多鏈之后)。

作為一個附加,對于長耗時的處理(比如上傳,長計算,批處理,等等),你可以用deferred.notify()作為then()第三個參數,給 promise 一個監聽來更新狀態。

鏈式 Promise

之前我們已經看過了,你可以給一個 promise 追加多個處理(then())。Promise API 好玩的地方在于允許鏈式處理:

舉個簡單的例子,這允許你把你的函數調用切分成單純的,單一目的方法,而不是一攬子麻團;還有另外一個好處是你可以在多 promise 任務中重用這些方法,就像你執行鏈式方法一樣(比如說任務列表之類的)。

如果你用前一個異步執行結果出發下一個異步處理,那就更牛X了。默認的,一個鏈式,像上面演示的那種,是會把前一個執行結果對象傳遞給下一個then()的。比如:

這會在控制臺輸出以下結果:

雖然例子簡單,但是你有沒有體會到如果then()返回另一個 promise 那種強大。這種情況下,下一個then()會在 promise 完結的時候被執行。這種模式可以用到把 HTTP 請求串上面,比如說(當一個請求依賴于前一個請求的結果的時候):

總結:

1、Promise 鏈會把上一個then的返回結果傳遞給調用鏈的下一個then(如果沒有就是 undefined)

2、如果then回掉返回一個 promise 對象,下一個then只會在這個 promise 被處理結束的時候調用。

3、在鏈最后的catch為整個鏈式處理提供一個異常處理點

4、在鏈最后的finally總是會被執行,不管 promise 被處理或者被拒絕,起清理作用

Parallel Promises And 'Promise-Ifying' Plain Values


我還提到了$q.all(),允許你等待并行的 promise 處理,當所有的 promise 都被處理結束之后,調用共同的回調。在 Angular 中,這個方法有兩種調用方式: 以Array方式或Object方式。Array方式接收多個 promise ,然后在調用.then()的時候使用一個數據結果對象,在結果對象里面包含了所有的 promise 結果,按照輸入數組的順序排列:

第二種方式是接收一個 promise 集合對象,允許你給每個 promise 一個別名,在回調函數中可以使用它們(有更好的可讀性):

我建議使用數組表示法,如果你只是希望可以批處理結果,就是說,如果你把所有的結果都平等處理。而以對象方式來處理,則更適合需要自注釋代碼的時候。

另一個有用的方法是$q.when(),如果你想通過一個普通變量創建一個 promise ,或者你不清楚你要處理的對象是不是 promise 時非常有用。

$q.when()在諸如服務中的緩存這種情況也很好用:

然后可以這樣調用它:

AngularJS 中的實際應用

在 Angular 的 I/O 中,大多數會返回 promise 或者 promise-compatible(then-able)對象,但是,都挺奇怪的。$http文檔說,它會返回一個HttpPromise對象,嗯,確實是 promise,但是有兩個額外的(有用的)方法,應該不會嚇到 jQuery 用戶。它定義了success()和error(),用來分別對應then()的第一和第二個參數。

Angular 的$resource服務,用于 REST-endpoints 的$http封裝,同樣有點奇怪;通用方法(get(),save()之類的四個)接收第二和第三個參數作為success和error回調,同時它們還返回一個對象,當請求被處理之后,會往其中填充請求的數據。它不會直接返回 promise 對象;相反,通過get()方法返回的對象有一個屬性$promise,用來暴露 promise 對象。

一方面,這和$http不符,并且 Angular 的所有東西都是/應該是 promise,不過另一方面,它允許開發者簡單的把$resource.get()的結果指派給$scope。原先,開發者可以給$scope指定任何 promise,但是從 Angular 1.2 開始被定義為過時了:請看this commit where it was deprecated

我個人來說,我更喜歡統一的 API,所以我把所有的 I/O 操作都封裝到了Service中,統一返回一個promise對象,不過調用$resource有點糙。下面是個例子:

這個例子有點晦澀,因為傳遞 id 參數給BarResource看起來有點多余,不過它也還是有道理的,比如你有一個復雜的對象,但只需要用它的 ID 屬性來調用一個服務。上面的好處還在于,在你的 controller 中,你知道從Service返回來的所有東西都是promise對象;你不需要擔心它到底是 promise 還是 resouce 或者是HttpPromise,這能讓你的代碼更加一致,并且可預測 - 因為 Javascript 是弱類型,并且到目前為止,據我所知沒有任何一款 IDE 能告訴你方法返回值的類型,它只能告訴你開發者寫了什么注釋,這點上面就非常重要了。

實際鏈式例子

我們的代碼庫有一部分是依賴于前一個調用的結果來執行的。Promise 非常適用這種情況,并且允許你書寫易于閱讀的代碼,盡可能保持你的代碼整潔。考慮如下例子:

聯合異步獲取數據(customers,carts,創建checkout)和處理同步數據(calculateTotals);這個實現不知道,甚至不需要知道這些服務是不是異步的,它會等到方法之行結束,不論異步與否。在這個例子中,getCart()會從本地存儲中獲取數據,createCheckout()會執行一個 HTTP 請求來確定產品的采購,諸如此類。不過從用戶的視角來看(執行這個調用的人),它不會關心這些;這個調用起作用了,并且它的狀態非常明了,你只要記住前一個調用會將結果返回傳遞到下一個then()。

當然,它就是自注釋代碼,并且很簡潔。

測試 Promise - 基于代碼

測試 Promise 非常簡單。你可以硬測,創建你的測試模擬對象,然后暴露then()方法,這種直接測法。但是,為了讓事情簡單,我只用了$q來創建promise- 這是一個非常快的庫。下面嘗試演示如何模擬上面用到過的各種服務。注意,這非常冗長,不過,我還沒有找出一個方法來解決它,除了在 promise 之外弄一些通用的方法(指針看起來更短更簡潔,會比較受歡迎)。

你看到咯,測試promise的代碼比它自己本身要長十倍;我不知道是否/或者有更簡單的代碼能達到同樣目的,不過,也許這里應該還有我沒找到(或者發布)的庫。

要獲取完整的測試覆蓋,需要為三個部分都編寫測試代碼,從失敗到處理結束,一個接一個,確保異常被記錄。雖然代碼中沒有很清楚演示,但是代碼/處理實際上會有許多分支;每個promise到最后都會被解決或者拒絕;真或假,或者被建立分支。不過,測試的粒度到底是由你決定的。

我希望這篇文章給大家帶來一些理解 promise 的啟示,以及教會怎樣結合 Angular 來使用 promise。我覺得我只摸到了 一些皮毛,包括在這篇文章以及在到目前為止我所做過的 AngularJS 工程上;promise 能夠擁有如此簡單的 API,如此簡單的概念,并且對大多數 Javascript 應用來說,有如此強大的力量和影響有點難以置信。結合高水平的通用方法,代碼庫,promise 可以讓你寫出更干凈,易于維護和易于擴展的代碼;添加一個句柄,改變它,改變實現方式,所有這些東西都很容易,如果你對 promise 的概念已經理解了的話。

從這點考慮,NodeJS 在開發早期就拋棄了 promise 而采用現在這種回調方式,我覺得非常古怪;當然我還沒有完全深入理解它,但是看起來好像是因為性能問題,不符合 Node 的原本目標的緣故。如果你把 NodeJS 當成一個底層的庫來看的話,我覺得還是很有道理的;有大量的庫可以為 Node 添加高級的 promise API(比如之前提到的 Q).

還有一點請記住,這篇文章是以 AngularJS 為基礎的,但是,promises和類promise編程方式已經在 Javascript 庫中存在好幾年了;jQuery,Deferreds 早在 jQuery 1.5 (1月 2011) 就被添加進來。雖然看起來一樣,但不是所有插件都能用。

同樣,Backbone.js的 Model Api 也暴露了promise在它的方法中(save()之類),但是,以我的理解,它貌似沒有沿著模型事件真正的起作用。也有可能我是錯的,因為已經有那么一段時間了。

如果開發一個新的 webapp 的時候,我肯定會推薦基于 promise 的前端應用的,因為它讓代碼看起來非常整潔,特別是結合函數式編程范式。還有更多功能強勁的編程模式可以在Reginald BraithwaiteJavascript Allongé book中找到,你可以從 LeanPub 拿到免費的閱讀副本;還有另外一些比較有用的基于 promise 的代碼。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容