AngularJS 中的 Promise 和 設(shè)計模式

其實在 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 BraithwaiteJavascript Allongé book中找到,你可以從 LeanPub 拿到免費的閱讀副本苹享;還有另外一些比較有用的基于 promise 的代碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末浴麻,一起剝皮案震驚了整個濱河市得问,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌软免,老刑警劉巖宫纬,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異膏萧,居然都是意外死亡漓骚,警方通過查閱死者的電腦和手機蝌衔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝌蹂,“玉大人噩斟,你說我怎么就攤上這事」赂觯” “怎么了剃允?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長齐鲤。 經(jīng)常有香客問我斥废,道長,這世上最難降的妖魔是什么给郊? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任牡肉,我火速辦了婚禮,結(jié)果婚禮上淆九,老公的妹妹穿的比我還像新娘统锤。我一直安慰自己,他們只是感情好吩屹,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布跪另。 她就那樣靜靜地躺著,像睡著了一般煤搜。 火紅的嫁衣襯著肌膚如雪免绿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天擦盾,我揣著相機與錄音嘲驾,去河邊找鬼。 笑死迹卢,一個胖子當著我的面吹牛辽故,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播腐碱,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼誊垢,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了症见?” 一聲冷哼從身側(cè)響起喂走,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谋作,沒想到半個月后芋肠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡遵蚜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年帖池,在試婚紗的時候發(fā)現(xiàn)自己被綠了奈惑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡睡汹,死狀恐怖肴甸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情帮孔,我是刑警寧澤雷滋,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站文兢,受9級特大地震影響晤斩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜姆坚,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一澳泵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧兼呵,春花似錦兔辅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至懂昂,卻和暖如春介时,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背凌彬。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工沸柔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人铲敛。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓褐澎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親伐蒋。 傳聞我的和親對象是個殘疾皇子工三,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

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