徹底理解 thunk 函式與 co 框架

abell123發表於2016-04-22

ES6 帶來了很多新的特性,其中生成器、yield等能對js金字塔式的非同步回撥做到很好地解決,而基於此封裝的co框架能讓我們完全以同步的方式來編寫非同步程式碼。這篇文章對生成器函式(GeneratorFunction)及框架thunkify、co的核心程式碼做了比較徹底的分析。co的使用還是比較廣泛的,除了我們日常的編碼要用到使我們的程式碼邏輯更清晰易懂外,一些知名框架也是基於co實現的,比如被稱為下一代的Nodejs web框架的koa等。

生成器函式

生成器函式是寫成:

格式的函式,其本質也是一個函式,所以它具備普通函式所具有的所有特性。除此之外,它還具有以下有用特性:

  1. 執行生成器函式後返回一個生成器(Generator),且生成器具有throw()方法,可手動丟擲一個異常,也常被用於判斷是否是生成器;
  2. 在生成器函式內部可以使用yield(或者yield*),函式執行到yield的時候都會暫停執行,並返回yield的右值(函式上下文,如變數的繫結等資訊會保留),通過生成器的next()方法會返回一個物件,含當前yield右邊表示式的值(value屬性),以及generator函式是否已經執行完(done屬性)等的資訊。每次執行next()方法,都會從上次執行的yield的地方往下,直到遇到下一個yield並返回包含相關執行資訊的物件後暫停,然後等待下一個next()的執行;
  3. 生成器的next()方法返回的是包含yield右邊表示式值及是否執行完畢資訊的物件;而next()方法的引數是上一個暫停處yield的返回值。

下面用例子說明:

例1:

根據上面說過的第3條執行準則:“生成器的next()方法返回的是包含yield右邊表示式值及是否執行完畢資訊的物件;而next()方法的引數是上一個暫停處yield的返回值”,因為我們沒有往生成器的next()中傳入任何值,所以:var a = yield ‘a’;中a的值為undefined。

那我們可以將例子稍微修改下:

例2:

這個就比較清晰明瞭了,不再做過多解釋。

關於yield*

yield暫停執行並只返回右值,而yield*則將函式委託到另一個生成器或可迭代的物件(如:字串、陣列、類陣列以及ES6的Map、Set等)。舉例如下:

arguments

Generator

thunk函式

在co的應用中,為了能像寫同步程式碼那樣書寫非同步程式碼,比較多的使用方式是使用thunk函式(但不是唯一方式,還可以是:Promise)。比如讀取檔案內容的一步函式fs.readFile()方法,轉化為thunk函式的方式如下:

那什麼叫thunk函式呢?

thunk函式具備以下兩個要素:

  1. 有且只有一個引數是callback的函式;
  2. callback的第一個引數是error。

使用thunk函式,同時結合co我們就可以像寫同步程式碼那樣來寫書寫非同步程式碼,先來個例子感受下:

是不是很酷?真的很酷!

其實,對於每次都去自己書寫一個thunk函式還是比較麻煩的,有一個框架thunkify可以幫我們輕鬆實現,修改後的程式碼如下:

對於thunkify框架的理解註釋如下:

程式碼並不複雜,看註釋應該就能看懂了。

co框架

我們先將對核心部分加了註釋的整個框架列出在下面,你可以先大概看下心裡有個數,也可以在下面分析整個執行邏輯後回過頭來細看:

下面,我們基於我們之前的例子對co的執行流程做一下分析。

我們的例子是:

首先,執行co()函式,內部除了快取當前執行上下文環境、除generator函式之外的引數處理,主要返回一個Promise例項:

我們主要看這個Promise內部做了什麼。

首先,判斷co()函式的第一個引數是否是函式,是的話將除gen之外的引數傳給該函式並返回給gen;在這裡因為gen是一個生成器函式,所以返回一個生成器;

後面判斷如果gen此時不是一個生成器,則直接執行Promise的resolve,其實就是將gen傳回給:co().then(function(val){});裡的val了;

我們這個例子gen是一個生成器,則繼續往下執行。

後面我們就遇到了co的核心函式:onFulfilled。我們看下這個函式做了什麼。

為了防止分心,裡面錯誤的處理我們先暫時不理。

第一次執行該方法,res值為undefined,然後執行生成器的next()方法,對應我們例子裡就是執行:

那麼ret是一個物件,大概是這樣:

然後將ret傳給next函式。next函式是:

首先判斷生成器內部是否已經執行完,執行完則將執行結果resolve出去。很明顯我們例子裡才執行到第一個yield,並沒有執行完。沒執行完,則將ret.value轉化為一個Promise例項,我們這裡是一個thunk函式,所以toPromise真正執行的是:

執行後其實就是直接返回了一個Promise例項。而這裡面,也對fn做了執行,fn是:function(cb){},對應到這裡,function(err, res){…}就是被傳入到fn中的cb,第一個引數就是error物件,第二個引數res就是讀取檔案後資料,然後執行resolve,將結果傳到下一個then方法的成功函式內,而在這裡對應的是:

其實也就是onFulFilled的引數res。根據上面第三條執行準則,我們知道,res是被傳入到生成器的next()方法裡的,其實也就是對應co內生成器函式引數裡的var a = yield readFile(‘a.txt’,{encoding:’utf8′});裡的a的值,從而實現了類似於同步的變成正規化。

這樣,整個基於thunk函式的co框架程式設計也就理通了,其他的Promise、Generator、GeneratorFunction、Object、Array模式的類似,不再做過多分析。

理解了co的執行邏輯,我們就能更好的掌握其用法,對於後續使用koa等基於co編寫的框架我們也能更快速地上手。

co的簡版

為了更方便快捷的理解co的執行邏輯,在網路上還有一個簡版的實現,如下:

但這個實現,僅支援yield後面是thunk函式的情形。使用示例:

會列印出:

希望能對理解和學習co的使用方法有很好的幫助。

以上。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

徹底理解 thunk 函式與 co 框架 徹底理解 thunk 函式與 co 框架

相關文章