天天看點

一篇需要膜拜的文篇--Javascript異步程式設計模型進化(轉)

要我能用得這麼熟,

那前端出師了哈。

http://foio.github.io/javascript-asyn-pattern/

改天一個一個親測一下。

Javascript語言是單線程的,沒有複雜的同步互斥;但是,這并沒有限制它的使用範圍;相反,借助于Node,Javascript已經在某些場景下具備通吃前後端的能力了。近幾年,多線程同步IO的模式已經在和單線程異步IO的模式的對決中敗下陣來,Node也是以得名。接下來我們深入介紹一下Javascript的殺手锏,異步程式設計的發展曆程。

讓我們假設一個應用場景:一篇文章有10個章節,章節的資料是通過XHR異步請求的,章節必須按順序顯示。我們從這個問題出發,逐漸探求從粗糙到優雅的解決方案。

在那個年代,javascript僅限于前端的簡單事件處理,這是異步程式設計的最基本模式了。 比如監聽dom事件,在dom事件發生時觸發相應的回調。

比如通過定時器執行異步任務。

但是這種模式注定無法處理複雜的業務邏輯的。假設有N個異步任務,每一個任務必須在上一個任務完成後觸發,于是就有了如下的代碼,這就産生了回調黑洞。

promise有then方法,可以添加在異步操作到達fulfilled狀态和rejected狀态的處理函數。

而then方法同時也會傳回一個promise對象,這樣我們就可以鍊式處理了。

MDN上的一張圖,比較清晰的描述了Pomise各個狀态之間的轉換。

一篇需要膜拜的文篇--Javascript異步程式設計模型進化(轉)

假設上文中的doAsyncJob都傳回一個promise對象,那我們看看如何用promise處理回調黑洞:

這種程式設計方式是不是清爽多了。我們最經常使用的jQuery已經實作了promise規範,在調用$.ajax時可以寫成這樣了:

我們可以使用ES6的Promise的構造函數生成自己的promise對象,Promise構造函數的參數為一個函數,該函數接收兩個函數(resolve,reject)作為參數,并在成功時調用resolve,失敗時調用reject。如下代碼生成一個擁有随機結果的promise。

promise錯誤處理也十分靈活,在promise構造函數中發生異常時,會自動設定promise的狀态為rejected,進而觸發相應的函數。

其中then(undefined,function(data)可以簡寫為catch。

promise的功能絕不僅限于上文這種小打小鬧的應用。對于篇頭提到的一篇文章10個章節異步請求,順序展示的問題,如果使用回調處理章節之間的依賴邏輯,顯然會産生回調黑洞; 而使用promise模式,則代碼形式優雅而且邏輯清晰。假設我們有一個包含10個章節内容的數組,并有一個傳回promise對象的getChaper函數:

下面我們探讨一下如何優雅高效的使用promise處理這個問題。

順序promise主要是通過對promise的then方法的鍊式調用産生的。

這種方法有一個問題,XHR請求是串行的,沒有充分利用浏覽器的并行性。網絡請求timeline和顯示效果圖如下:

一篇需要膜拜的文篇--Javascript異步程式設計模型進化(轉)
一篇需要膜拜的文篇--Javascript異步程式設計模型進化(轉)

Promise類有一個all方法,其接受一個promise數組:

隻有promise數組中的promise全部兌現,才會調用then方法。使用Promise.all,我們可以并發性的進行網絡請求,并在所有請求傳回後在集中進行資料展示。

這種方法也有一個問題,要等到所有資料加載完成後,才會一次性展示全部章節。效果圖如下:

一篇需要膜拜的文篇--Javascript異步程式設計模型進化(轉)
一篇需要膜拜的文篇--Javascript異步程式設計模型進化(轉)

其實,我們可以做到并發的請求資料,盡快展示滿足順序條件的章節:即前面的章節展示後就可以展示目前章節,而不用等待後續章節的網絡請求。基本思路是:先建立一批并行的promise,然後通過鍊式調用then方法控制展示順序。

效果如下:

一篇需要膜拜的文篇--Javascript異步程式設計模型進化(轉)
一篇需要膜拜的文篇--Javascript異步程式設計模型進化(轉)

這三種模式基本上概括了使用Pormise控制并發的方式,你可以根據業務需求,确定各個任務之間的依賴關系,進而做出選擇。

異步程式設計的一種解決方案叫做"協程"(coroutine),意思是多個線程互相協作,完成異步任務。随着ES6中對協程的支援,這種方案也逐漸進入人們的視野。Generator函數是協程在 ES6 的實作.

讓我們先從三個方面了解generator。

在普通函數名前面加*号就可以生成generator函數,該函數傳回一個指針,每一次調用next函數,就會移動該指針到下一個yield處,直到函數結尾。通過next函數就可以控制generator函數的執行。如下所示:

next函數傳回一個對象{value:'love',done:false},其中value表示yield傳回值,done表示generator函數是否執行完成。這樣寫有點low?試試這種文法。

next()函數中可以傳遞參數,作為yield的傳回值,傳遞到函數體内部。這裡有點tricky,next參數作為上一次執行yeild的傳回值。了解“上一次”很重要。

比如這裡的g.next(2),參數2為上一步yield x + 1 的傳回值賦給y,進而我們就可以在接下來的代碼中使用。這就是generator資料傳遞的基本方法了。

通過generator函數傳回的指針,我們可以向函數内部傳遞異常,這也使得異步任務的異常處理機制得到保證。

仍然使用本文中的getChapter方法,該方法傳回一個promise,我們看一下如何使用generator處理異步回調。gen方法在執行到yield指令時傳回的result.value是promise對象,然後我們通過next方法将promise的結果傳回到gen函數中,作為addToPage的參數。

gen函數的代碼,和普通同步函數幾乎沒有差別,隻是多了一條yield指令。

這種方法的效果類似于上文中提到“順序promise”,我們能不能實作上文的“并發promise,漸進式”呢?代碼如下:

經曆過複雜性才能達到簡單性。我們從最開始的回調黑洞到最終的generator,越來越複雜也越來越簡單。

===================

一篇需要膜拜的文篇--Javascript異步程式設計模型進化(轉)

繼續閱讀