其實在 Javascript 中钓觉,有另外一種異步處理模式:更屌蝗蛙,在 Javascript 里面經(jīng)常被叫做Promises, CommonJS 標準委員會于是發(fā)布了一個規(guī)范揪漩,就把這個 API 叫做Promises了技俐。
Promise 背后的概念非常簡單乘陪,有兩部分:
1、Deferreds雕擂,定義工作單元啡邑,
2、Promises井赌,從 Deferreds 返回的數(shù)據(jù)谤逼。
基本上,你會用 Deferred 作為通信對象族展,用來定義工作單元的開始森缠,處理和結(jié)束三部分。
Promise 是 Deferred 響應(yīng)數(shù)據(jù)的輸出仪缸;它有狀態(tài) (等待贵涵,執(zhí)行和拒絕),以及句柄恰画,或叫做回調(diào)函數(shù)宾茂,反正就是那些在 Promise 執(zhí)行,拒絕或者提示進程中會被調(diào)用的方法拴还。
Promise 不同于回調(diào)的很重要的一個點是跨晴,你可以在 Promise 狀態(tài)變成執(zhí)行(resolved)之后追加處理句柄。這就允許你傳輸數(shù)據(jù)片林,而忽略它是否已經(jīng)被應(yīng)用獲取端盆,然后緩存它,等等之類的操作费封,因此你可以對數(shù)據(jù)執(zhí)行操作焕妙,而不管它是否已經(jīng)或者即將可用。
在之后的文章中弓摘,我們將會基于 AngularJS 來講解 Promises 焚鹊。AngularJS 的整個代碼庫很大程度上依賴于 Promise,包括框架以及你用它編寫的應(yīng)用代碼韧献。AngularJS 用的是它自己的 Promises 實現(xiàn)末患,$q服務(wù)研叫,又一個 Q 庫的輕量實現(xiàn)。
$q實現(xiàn)了上面提到的所有 Deferred / Promise 方法璧针,除此之外$q還有自己的實現(xiàn):$q.defer()嚷炉,用來創(chuàng)建一個新的 Deferred 對象;$q.all()陈莽,允許等待多 Promises 執(zhí)行終了渤昌,還有方法$q.when()和$q.reject()虽抄,具體我們之后會講到走搁。
$q.defer()返回一個 Deferred 對象,帶有方法resolve(),reject(), 和notify()迈窟。Deferred 還有一個promise屬性私植,這是一個promise對象,可以用于應(yīng)用內(nèi)部傳遞车酣。
promise 對象有另外三個方法:.then()曲稼,是唯一 Promise 規(guī)范要求的方法,用三個回調(diào)方法作為參數(shù)湖员;一個成功回調(diào)贫悄,一個失敗回調(diào),還有一個狀態(tài)變化回調(diào)娘摔。
$q在 Promise 規(guī)范之上還添加了兩個方法:catch()窄坦,可以用于定義一個通用方法,它會在 promise 鏈中有某個 promise 處理失敗時被調(diào)用凳寺。還有finally()鸭津,不管 promise 執(zhí)行是成功或者失敗都會執(zhí)行。注意肠缨,這些不應(yīng)該和 Javascript 的異常處理混淆或者并用: 在 promise 內(nèi)部拋出的異常逆趋,不會被catch()俘獲。(※貌似這里我理解錯了)
Promise 簡單例子
下面是使用$q晒奕,Deferred闻书,和Promise放一起的簡單例子。首先我要聲明脑慧,本文中所有例子的代碼都沒有經(jīng)過測試魄眉;而且也沒有正確的引用Angular服務(wù)和依賴,之類的漾橙。不過我覺得對于啟發(fā)你怎么玩杆融,已經(jīng)夠好了。
首先霜运,我們先創(chuàng)建一個新的工作單元脾歇,通過 Deferred 對象蒋腮,用$q.defer():
然后,我們從 Deferred 拿到promise藕各,給它追加一些行為池摧。
最后,我們假裝做點啥激况,然后告訴 deferred 我們已經(jīng)完成了:
當然作彤,這不需要真的異步,所以我們可以用 Angular 的$timeout服務(wù)(或者 Javascript 的setTimeout乌逐,不過竭讳,在 Angular 應(yīng)用中最好用$timeout,這樣你可以 mock/test 它)來假裝一下浙踢。
好了绢慢,有趣的是:我們可以追加很多個then()到一個 promise 上,以及我們可以在 promise 被 resolved 之后追加then():
那洛波,要是發(fā)生異常怎么辦胰舆?我們用deferred.reject(),它會出發(fā)then()的第二個函數(shù)蹬挤,就像回調(diào)一樣缚窿。
不用then()的第二個參數(shù),還有另外一種選擇焰扳,你可以用鏈式的catch()倦零,在 promise 鏈中發(fā)生異常的時候它會被調(diào)用(可能在很多鏈之后)。
作為一個附加蓝翰,對于長耗時的處理(比如上傳光绕,長計算,批處理畜份,等等)诞帐,你可以用deferred.notify()作為then()第三個參數(shù),給 promise 一個監(jiān)聽來更新狀態(tài)爆雹。
鏈式 Promise
之前我們已經(jīng)看過了停蕉,你可以給一個 promise 追加多個處理(then())。Promise API 好玩的地方在于允許鏈式處理:
舉個簡單的例子钙态,這允許你把你的函數(shù)調(diào)用切分成單純的慧起,單一目的方法,而不是一攬子麻團册倒;還有另外一個好處是你可以在多 promise 任務(wù)中重用這些方法蚓挤,就像你執(zhí)行鏈式方法一樣(比如說任務(wù)列表之類的)。
如果你用前一個異步執(zhí)行結(jié)果出發(fā)下一個異步處理,那就更牛X了灿意。默認的估灿,一個鏈式,像上面演示的那種缤剧,是會把前一個執(zhí)行結(jié)果對象傳遞給下一個then()的馅袁。比如:
這會在控制臺輸出以下結(jié)果:
雖然例子簡單,但是你有沒有體會到如果then()返回另一個 promise 那種強大荒辕。這種情況下汗销,下一個then()會在 promise 完結(jié)的時候被執(zhí)行。這種模式可以用到把 HTTP 請求串上面抵窒,比如說(當一個請求依賴于前一個請求的結(jié)果的時候):
總結(jié):
1弛针、Promise 鏈會把上一個then的返回結(jié)果傳遞給調(diào)用鏈的下一個then(如果沒有就是 undefined)
2、如果then回掉返回一個 promise 對象估脆,下一個then只會在這個 promise 被處理結(jié)束的時候調(diào)用钦奋。
3、在鏈最后的catch為整個鏈式處理提供一個異常處理點
4疙赠、在鏈最后的finally總是會被執(zhí)行,不管 promise 被處理或者被拒絕朦拖,起清理作用
Parallel Promises And 'Promise-Ifying' Plain Values
我還提到了$q.all()圃阳,允許你等待并行的 promise 處理,當所有的 promise 都被處理結(jié)束之后璧帝,調(diào)用共同的回調(diào)捍岳。在 Angular 中,這個方法有兩種調(diào)用方式: 以Array方式或Object方式睬隶。Array方式接收多個 promise 锣夹,然后在調(diào)用.then()的時候使用一個數(shù)據(jù)結(jié)果對象,在結(jié)果對象里面包含了所有的 promise 結(jié)果苏潜,按照輸入數(shù)組的順序排列:
第二種方式是接收一個 promise 集合對象银萍,允許你給每個 promise 一個別名,在回調(diào)函數(shù)中可以使用它們(有更好的可讀性):
我建議使用數(shù)組表示法恤左,如果你只是希望可以批處理結(jié)果贴唇,就是說,如果你把所有的結(jié)果都平等處理飞袋。而以對象方式來處理戳气,則更適合需要自注釋代碼的時候。
另一個有用的方法是$q.when()巧鸭,如果你想通過一個普通變量創(chuàng)建一個 promise 瓶您,或者你不清楚你要處理的對象是不是 promise 時非常有用。
$q.when()在諸如服務(wù)中的緩存這種情況也很好用:
然后可以這樣調(diào)用它:
AngularJS 中的實際應(yīng)用
在 Angular 的 I/O 中,大多數(shù)會返回 promise 或者 promise-compatible(then-able)對象呀袱,但是芯肤,都挺奇怪的。$http文檔說压鉴,它會返回一個HttpPromise對象崖咨,嗯,確實是 promise油吭,但是有兩個額外的(有用的)方法击蹲,應(yīng)該不會嚇到 jQuery 用戶。它定義了success()和error()婉宰,用來分別對應(yīng)then()的第一和第二個參數(shù)歌豺。
Angular 的$resource服務(wù),用于 REST-endpoints 的$http封裝心包,同樣有點奇怪类咧;通用方法(get(),save()之類的四個)接收第二和第三個參數(shù)作為success和error回調(diào),同時它們還返回一個對象蟹腾,當請求被處理之后痕惋,會往其中填充請求的數(shù)據(jù)。它不會直接返回 promise 對象娃殖;相反值戳,通過get()方法返回的對象有一個屬性$promise,用來暴露 promise 對象炉爆。
一方面堕虹,這和$http不符,并且 Angular 的所有東西都是/應(yīng)該是 promise芬首,不過另一方面赴捞,它允許開發(fā)者簡單的把$resource.get()的結(jié)果指派給$scope。原先郁稍,開發(fā)者可以給$scope指定任何 promise赦政,但是從 Angular 1.2 開始被定義為過時了:請看this commit where it was deprecated。
我個人來說艺晴,我更喜歡統(tǒng)一的 API昼钻,所以我把所有的 I/O 操作都封裝到了Service中,統(tǒng)一返回一個promise對象封寞,不過調(diào)用$resource有點糙然评。下面是個例子:
這個例子有點晦澀,因為傳遞 id 參數(shù)給BarResource看起來有點多余狈究,不過它也還是有道理的碗淌,比如你有一個復(fù)雜的對象,但只需要用它的 ID 屬性來調(diào)用一個服務(wù)。上面的好處還在于亿眠,在你的 controller 中碎罚,你知道從Service返回來的所有東西都是promise對象;你不需要擔心它到底是 promise 還是 resouce 或者是HttpPromise纳像,這能讓你的代碼更加一致荆烈,并且可預(yù)測 - 因為 Javascript 是弱類型,并且到目前為止竟趾,據(jù)我所知沒有任何一款 IDE 能告訴你方法返回值的類型憔购,它只能告訴你開發(fā)者寫了什么注釋,這點上面就非常重要了岔帽。
實際鏈式例子
我們的代碼庫有一部分是依賴于前一個調(diào)用的結(jié)果來執(zhí)行的玫鸟。Promise 非常適用這種情況,并且允許你書寫易于閱讀的代碼犀勒,盡可能保持你的代碼整潔屎飘。考慮如下例子:
聯(lián)合異步獲取數(shù)據(jù)(customers,carts,創(chuàng)建checkout)和處理同步數(shù)據(jù)(calculateTotals)贾费;這個實現(xiàn)不知道钦购,甚至不需要知道這些服務(wù)是不是異步的,它會等到方法之行結(jié)束铸本,不論異步與否肮雨。在這個例子中,getCart()會從本地存儲中獲取數(shù)據(jù)箱玷,createCheckout()會執(zhí)行一個 HTTP 請求來確定產(chǎn)品的采購,諸如此類陌宿。不過從用戶的視角來看(執(zhí)行這個調(diào)用的人)锡足,它不會關(guān)心這些;這個調(diào)用起作用了壳坪,并且它的狀態(tài)非常明了舶得,你只要記住前一個調(diào)用會將結(jié)果返回傳遞到下一個then()。
當然爽蝴,它就是自注釋代碼沐批,并且很簡潔。
測試 Promise - 基于代碼
測試 Promise 非常簡單蝎亚。你可以硬測九孩,創(chuàng)建你的測試模擬對象,然后暴露then()方法发框,這種直接測法躺彬。但是,為了讓事情簡單,我只用了$q來創(chuàng)建promise- 這是一個非诚苡担快的庫仿野。下面嘗試演示如何模擬上面用到過的各種服務(wù)。注意她君,這非常冗長脚作,不過,我還沒有找出一個方法來解決它缔刹,除了在 promise 之外弄一些通用的方法(指針看起來更短更簡潔,會比較受歡迎)球涛。
你看到咯,測試promise的代碼比它自己本身要長十倍;我不知道是否/或者有更簡單的代碼能達到同樣目的桨螺,不過宾符,也許這里應(yīng)該還有我沒找到(或者發(fā)布)的庫。
要獲取完整的測試覆蓋灭翔,需要為三個部分都編寫測試代碼魏烫,從失敗到處理結(jié)束,一個接一個肝箱,確保異常被記錄哄褒。雖然代碼中沒有很清楚演示,但是代碼/處理實際上會有許多分支煌张;每個promise到最后都會被解決或者拒絕呐赡;真或假,或者被建立分支骏融。不過链嘀,測試的粒度到底是由你決定的。
我希望這篇文章給大家?guī)硪恍├斫?promise 的啟示档玻,以及教會怎樣結(jié)合 Angular 來使用 promise怀泊。我覺得我只摸到了 一些皮毛,包括在這篇文章以及在到目前為止我所做過的 AngularJS 工程上;promise 能夠擁有如此簡單的 API误趴,如此簡單的概念霹琼,并且對大多數(shù) Javascript 應(yīng)用來說,有如此強大的力量和影響有點難以置信凉当。結(jié)合高水平的通用方法枣申,代碼庫,promise 可以讓你寫出更干凈看杭,易于維護和易于擴展的代碼忠藤;添加一個句柄,改變它泊窘,改變實現(xiàn)方式熄驼,所有這些東西都很容易像寒,如果你對 promise 的概念已經(jīng)理解了的話。
從這點考慮瓜贾,NodeJS 在開發(fā)早期就拋棄了 promise 而采用現(xiàn)在這種回調(diào)方式诺祸,我覺得非常古怪;當然我還沒有完全深入理解它,但是看起來好像是因為性能問題祭芦,不符合 Node 的原本目標的緣故筷笨。如果你把 NodeJS 當成一個底層的庫來看的話,我覺得還是很有道理的龟劲;有大量的庫可以為 Node 添加高級的 promise API(比如之前提到的 Q).
還有一點請記住胃夏,這篇文章是以 AngularJS 為基礎(chǔ)的,但是昌跌,promises和類promise編程方式已經(jīng)在 Javascript 庫中存在好幾年了仰禀;jQuery,Deferreds 早在 jQuery 1.5 (1月 2011) 就被添加進來蚕愤。雖然看起來一樣答恶,但不是所有插件都能用。
同樣萍诱,Backbone.js的 Model Api 也暴露了promise在它的方法中(save()之類)悬嗓,但是,以我的理解裕坊,它貌似沒有沿著模型事件真正的起作用包竹。也有可能我是錯的,因為已經(jīng)有那么一段時間了籍凝。
如果開發(fā)一個新的 webapp 的時候周瞎,我肯定會推薦基于 promise 的前端應(yīng)用的,因為它讓代碼看起來非常整潔饵蒂,特別是結(jié)合函數(shù)式編程范式堰氓。還有更多功能強勁的編程模式可以在Reginald Braithwaite的Javascript Allongé book中找到,你可以從 LeanPub 拿到免費的閱讀副本苹享;還有另外一些比較有用的基于 promise 的代碼。