天天看點

Promise的前世今生和妙用技巧

JavaScript作為單線程運作于浏覽器之中,這是每本JavaScript教科書中都會被提到的。同時出于對UI線程操作的安全性考慮,JavaScript和UI線程也處于同一個線程中。是以對于長時間的耗時操作,将會阻塞UI的響應。為了更好的UI體驗,應該盡量的避免JavaScript中執行較長耗時的操作(如大量for循環的對象diff等)或者是長時間I/O阻塞的任務。是以在浏覽器中的大多數任務都是異步(無阻塞)執行的,例如:滑鼠點選事件、視窗大小拖拉事件、定時器觸發事件、Ajax完成回調事件等。當每一個異步事件完成時,它都将被放入一個叫做”浏覽器事件隊列“中的事件池中去。而這些被放在事件池中的任務,将會被javascript引擎單線程處理的一個一個的處理,當在此次進行中再次遇見的異步任務,它們也會被放到事件池中去,等待下一次的tick被處理。另外在HTML5中引入了新的元件-Web Worker,它可以在JavaScript線程以外執行這些任務,而不阻塞目前UI線程。

浏覽器中的事件循環模型如下圖所示:

Promise的前世今生和妙用技巧

由于浏覽器的這種内部事件循環機制,是以JavaScript一直以callback回調的方式來處理事件任務。是以無所避免的對于多個的JavaScript異步任務的處理,将會遇見”callback hell“(回調地獄),使得這類代碼及其不可讀和難易維護。

Promise的前世今生和妙用技巧

是以很多JavaScript牛人開始尋找解決這回調地獄的模式設計,随後Promise(jQuery的<code>deferred</code>也屬于Promise範疇)便被引入到了JavaScript的世界。Promise在英語中語義為:”承諾“,它表示如A調用一個長時間任務B的時候,B将傳回一個”承諾“給A,A不用關心整個實施的過程,繼續做自己的任務;當B實施完成的時候,會通過A,并将執行A之間的預先約定的回調。而deferred在英語中語義為:”延遲“,這也說明promise解決的問題是一種帶有延遲的事件,這個事件會被延遲到未來某個合适點再執行。

Promise 對象有三種狀态: Pending – Promise對象的初始狀态,等到任務的完成或者被拒絕;Fulfilled – 任務執行完成并且成功的狀态;Rejected – 任務執行完成并且失敗的狀态;

Promise的狀态隻可能從“Pending”狀态轉到“Fulfilled”狀态或者“Rejected”狀态,而且不能逆向轉換,同時“Fulfilled”狀态和“Rejected”狀态也不能互相轉換;

Promise對象必須實作then方法,then是promise規範的核心,而且then方法也必須傳回一個Promise對象,同一個Promise對象可以注冊多個then方法,并且回調的執行順序跟它們的注冊順序一緻;

then方法接受兩個回調函數,它們分别為:成功時的回調和失敗時的回調;并且它們分别在:Promise由“Pending”狀态轉換到“Fulfilled”狀态時被調用和在Promise由“Pending”狀态轉換到“Rejected”狀态時被調用。

如下面所示:

Promise的前世今生和妙用技巧

Promise将原來回調地獄中的回調函數,從橫向式增加巧妙的變為了縱向增長。以鍊式的風格,縱向的書寫,使得代碼更加的可讀和易于維護。

Promise在JavaScript的世界中逐漸的被大家所接受,是以在ES6的标準版中已經引入了Promise的規範了。現在通過Babel,可以完全放心的引入産品環境之中了。

如上所說Promise在處理異步回調或者是延遲執行任務時候,是一個不錯的選擇方案。下面我們将介紹一些Promise的使用技巧(下面将利用Angular的<code>$q</code>和<code>$http</code>為例,當然對于jQuery的<code>deferred</code>,ES6的Promise仍然實用):

在上文中提到的回調地獄案例,就是一種試圖去将多個異步的任務串行處理的結果,使得代碼不斷的橫向延伸,可讀性和維護性急劇下降。當然我們也提到了Promise利用鍊式和延遲執行模型,将代碼從橫向延伸拉回了縱向增長。使用Angular中<code>$http</code>的實作如下:

因為Promise是可以傳遞的,可以繼續<code>then</code>方法延續下去,也可以在縱向擴充的途中改變為其他Promise或者資料。是以在例子中的$http也可以換為其他的Promise(如$<code>timeout</code>,<code>$resource</code> …)。

在有些場景下,我們所要處理的多個異步任務之間并沒有像上例中的那麼強的依賴關系,隻需要在這一系列的異步任務全部完成的時候執行一些特定邏輯。這個時候為了性能的考慮等,我們不需要将它們都串行起來執行,并行執行它們将是一個最優的選擇。如果仍然采用回調函數,則這是一個非常惱人的問題。利用Promise則同樣可以優雅的解決它:

這樣就可以等到一堆異步的任務完成後,在執行特定的業務回調了。在Angular中的路由機制<code>ngRoute</code>、<code>uiRoute</code>的resolve機制也是采用同樣的原理:在路由執行的時候,會将擷取模闆的Promise、擷取所有resolve資料的Promise都拼接在一起,同時并行的擷取它們,然後等待它們都結束的時候,才開始初始化<code>ng-view</code>、<code>ui-view</code>指令的scope對象,以及compile模闆節點,并插入頁面DOM中,完成一次路由的跳轉并且切換了View,将靜态的HTML模闆變為動态的網頁展示出來。

Angular路由機制的僞代碼如下:

有了Promise的處理,因為在前端代碼中最多的異步處理就是Ajax,它們都被包裝為了Promise .then的風格。那麼對于一部分同步的非異步處理呢?如localStorage、setTimeout、setInterval之類的方法。在大多數情況下,部落客仍然推薦使用Promise的方式包裝,使得項目Service的傳回接口統一。這樣也便于像上例中的多個異步任務的串行、并行處理。在Angular路由中對于隻設定template的情況,也是這麼處理的。

對于setTimeout、setInterval在Angular中都已經為我們内置了$timeout和$interval服務,它們就是一種Promise的封裝。對于localStorage呢?可以采用$q.when方法來直接包裝localStorage的傳回值的為Promise接口,如下所示:

整個項目的Service層的傳回值都可以被封裝為統一的風格使用了,項目變得更加的一緻和統一。在需要多個Service同時并行或者串行處理的時候,也變得簡單了,一緻的使用方式。

在前面已經提到Promise是延遲到未來執行某些特定任務,在調用時候則給消費者傳回一個”承諾“,消費者線程并不會被阻塞。在消費者接受到”承諾“之後,消費者就不用再關心這些任務是如何完成的,以及督促生産者的任務執行狀态等。直到任務完成後,消費者手中的這個”承諾“就被兌現了。

對于這類延遲機制,在前端的UI互動中也是極其常見的。比如模态視窗的顯示,對于使用者在模态視窗中的互動結果并不可提前預知的,使用者是點選”ok“按鈕,或者是”cancel“按鈕,這是一個未來将會發生的延遲事件。對于這類場景的處理,也是Promise所擅長的領域。在Angular-UI的Bootstrap的modal的實作也是基于Promise的封裝。

這是因為modal在open方法的傳回值中給了我們一個Promise的result對象(承諾)。等到使用者在模态視窗中點選了ok按鈕,則Bootstrap會使用<code>$q</code>的<code>defer</code>來<code>resolve</code>來執行ok事件;相反,如果使用者點選了cancel按鈕,則會使用<code>$q</code>的<code>defer</code>來<code>reject</code>執行cancel事件。

這樣就很好的解決了延遲觸發的問題,也避免了<code>callback的地獄</code>。我們仍然可以進一步将其傳回值語義化,以業務自有的術語命名而形成一套DSL API。

則我們可以如下方式來通路它:

是不是感覺更具有語義呢?在Angular中$http的傳回方法success、error也是同樣邏輯的封裝。将success的注冊函數注冊為.then方法的成功回調,error的注冊方法注冊為then方法的失敗回調。是以success和error方法隻是Angular架構為我們在Promise文法之上封裝的一套文法糖而已。

Angular的success、error回調的實作代碼:

在Angular中同樣也内置了一些AOP的設計思想,便于實作程式通用功能與業務子產品的分離、解耦、統一處理和維護。$http中的攔截器(interceptors)和裝飾器($provide.decorator)是Angular中兩類常見的AOP切入點。前者以管道式執行政策實作,而後者則通過運作時的Promise管道動态實作的。

首先回顧一下Angular的攔截器實作方式:

這樣就可以實作對Angular中的<code>$http</code>或者是<code>$resource</code>的Ajax請求攔截了。但在Angular内部是是如何實作這種攔截方式的呢?Angular使用的就是Promise機制,形成異步管道流,将真實的Ajax請求放置在request、requestError和response、responseError的管道中間,是以就産生了對Ajax請求的攔截。

其源碼實作如下:

在上面緊接着在<code>$get</code>注入方法之後,Angular會将<code>interceptors</code>和<code>responseInterceptors</code>反轉合并到一個<code>reversedInterceptors</code>的攔截器内部變量中儲存。最後在$http函數中以<code>[serverRequest, undefined]</code>(<code>serverRequest</code>是Ajax請求的Promise操作)為中心,将<code>reversedInterceptors</code>中的所有攔截器函數依次加入到chain鍊式數組中。如果是request或requestError,那麼就放在鍊式數組起始位置;相反如果是response或responseError,那麼就放在鍊式數組最後。

注意添加在chain的request和requestError或者response和responseError都一定是成對的,換句話說可能注冊一個非空的request與一個為undefined的requestError,或者是一個為undefined的request與非空的requestError。就像chain數組的聲明一樣<code>(var chain = [serverRequest, undefined];)</code>,成對的放入serverRequest和undefined對象到數組中。因為後面的代碼将利用Promise的機制注冊這些攔截器函數,實作管道式AOP攔截機制。

在Promise中需要兩個函數來注冊回調,分别為成功回調和失敗回調。在這裡request和response會被注冊成Promise的成功回調,而requestError和responseError則會注冊成Promise的失敗回調。是以在chain中添加的request和requestError,response或responseError都是成對出現的,這是為了能在接下來的循環中簡潔地注冊Promise回調函數。 這些被注冊的攔截器鍊,會通過<code>$q.when(config)</code> Promise啟動,它會首先傳入<code>$http</code>的config對象,并執行所有的request攔截器,依次再到<code>serverRequest</code>這個Ajax請求,此時将挂起後邊所有的response攔截器,直到Ajax請求響應完成,再依次執行剩下的response攔截器回調; 如果在request過程中有異常失敗則會執行後邊的requestError攔截器,對于Ajax請求的失敗或者處理Ajax的response攔截器的異常也會被後面注冊的responseError攔截器捕獲。

從最後兩段代碼也能了解到關于<code>$http</code>服務中的success方法和error方法,是Angular為大家提供了一種Promise的便捷寫法。success方法是注冊一個傳入的成功回調和為undefined的錯誤回調,而error則是注冊一個為null的成功回調和一個傳入的失敗回調。

另外,同時也歡迎關注部落客的微信公衆号[破狼](微信二維碼位于部落格右側),這裡将會為大家地時間推送部落客的最新博文,謝謝大家的支援和鼓勵。

本文轉自破狼部落格園部落格,原文連結:http://www.cnblogs.com/whitewolf/p/promise-best-practice.html,如需轉載請自行聯系原作者

繼續閱讀