Node.js中基本都是異步編程,我們回想下爲什麼初學者很容易寫出深度嵌套callback的代碼?因爲直觀啊,一眼即懂。當然實際寫的時候肯定不推薦callback套callback,需要一個工具來把一個任務完整的串起來。
我們知道,在項目管理裏面有一個很出名的理論,叫番茄工作法(不知道的自行google),它所做的事情是把未來一段時間要做的事情都按照時間段拆分成一個個很小的任務,然後逐個完成。
stepify設計思路和番茄工作法有些類似,都是先定義好了任務最後再執行,不同的是前者以單個異步操作作粒度劃分,後者只是以時間劃分。
想想現實中我們怎麼去做事的:比如做飯這件不大不小的事兒,因爲只有一個煤氣竈,所以炒菜只能炒完一個炒下一個菜,但是同時我們有個電飯煲,所以可以在炒菜的同時煮飯。這樣子下來,做一頓飯的總時間就是max(煮飯的時間, 燒菜的時間)。而在炒菜的過程中,沒有規定一定要先西紅柿炒蛋完了之後再蛋炒番茄。這個做飯的過程,可以類比成上文所說的工作流,煮飯和燒菜是兩個並行的task,每燒一個菜算是完成一個step。
stepify中的每一個異步任務執行的時機是前一個異步操作執行完而且沒遇到異常,如果遇到異常,則把異常交給事先定義好的異常處理函數,在異常處理函數裏可以決定是否繼續執行下一個任務(比如燒菜時發現沒了醬油,你可以決定是否還繼續炒這道菜還是換下一道)。
抽象了?直接看代碼文檔吧,代碼託管在 github 。
stepify目前已經發布到 npm ,可以使用npm直接安裝:
$ npm install stepify
用法:
假設有一個工作(work )需要完成,它分解爲 task1 、 task2 、 task3 。。。幾個任務,每個任務分爲好幾個步驟( step ),使用 stepify 實現的僞代碼如下:
var workflow = Stepify() .task('t1') .step('t1s1', fn) // t1s1的執行結果可以通過fn內部的`this.done`方法傳給t1s2,下同 .step('t1s2', fn) .step('s', fn) .task('t2') .step('t2s1', fn) .step('t2s2', fn) .step('s', fn) // 定義任務t2的異常處理函數 .error(fn) .task('t3') .step('t3s1', fn) .step('t3s2', fn) // pend是指結束一個task的定義,接下來定義下一個task或者一些公共方法 // task裏邊其實會先調用下pend以自動結束上一個task的定義 .pend() // 處理work中每一個task的每一個(異步)step異常 .error(fn) // 處理最終結果,result()是可選的(不需要關注輸出的情況) .result(fn) .run()
這裏可以看到,工作原理很簡單,就是**先定義好後執行**。
解釋下,pend的作用是分割task的定義,表示這個task要具體怎麼做已經定義好了。裏邊還有兩個`error()`的調用,跟在t2後面error調用的表明t2的異常由傳入這個error的函數來處理,t1和t3沒有顯示定義error,所以它們的異常將交給後面一個error定義的函數來處理,這個是不是很像js的時間冒泡?
默認情況下,work的執行是按照task定義的順序來串行執行的,所以還可以這樣簡化:
var workflow = Stepify() .step('t1s1', fn) .step('t1s2', fn) .step('s', fn) .pend() .step('t2s1', fn) .step('t2s2', fn) .step('s', fn) .error(fn) .pend() .step('t3s1', fn) .step('t3s2', fn) .pend() .error(fn) .result(fn) .run()
細心的童靴可能已經發現,t1和t2後面的都有一個step——`step(‘s’, fn)`,這裏其實還可以把它抽出來:
var workflow = Stepify() .step('t1s1', fn) .step('t1s2', fn) .step('s') .pend() .step('t2s1', fn) .step('t2s2', fn) .step('s') .error(fn) .pend() .step('t3s1', fn) .step('t3s2', fn) .pend() .s(fn) .error(fn) .result(fn) .run()
是不是很神奇?s並不是stepify內置的方法而是動態擴展出來的!
那接下來又有個問題,t1和t2都有執行兩個`step(‘s’)`,那額外的參數怎麼傳遞呢?奧妙之處在於step函數,它後面還可以跟其他參數,表示在我們定義所有task之前就已經知道的變量(我叫它⎡靜態參數⎦),還有任務執行過程中,如果上一個step的輸出怎麼傳遞給下一個step呢?答案是 next 或者 done ,具體可以參考api,`s(fn)`只是定義一個函數體,通過靜態參數和動態參數結合,可以實現不同的操作。
這還沒完,我們都聽過一句話,叫做“條條大路通羅馬(All roads lead to Rome)”,解決問題的方式往往有多種。上面這個例子,假如外部條件變了,task1和task2它們的執行互不影響,task3的執行需要依賴task1和task2的結果,即task1和task2可以並行,這樣子怎麼實現呢?
很簡單,奧妙在run方法:
run(['t1', 't2'], 't3');
把t1和t2放到數組中,它們便是並行執行!同理,可以變出很多種組合來。
至於很多人問的和async的區別和優勢,這不是一兩句話解釋的清楚的,設計理念不同,二者並不衝突,async在併發控制上面很優秀,而stepify則重在流程控制,裏面也有簡單的 parallel 支持。
可以看到,一個複雜的工作流,通過stepify定製,每一步都是那麼清晰可讀!