1. 程式人生 > >【javascipt】Generator函式(生成器)

【javascipt】Generator函式(生成器)

key: Generator函式是ES6提供的一種非同步程式設計解決方案

Generator函式和普通函式就有兩個區別: 

        一是  function命令與函式名之間有一個星號 *   

        二是 函式體內有yield語句 定義不同的內部狀態

----------------------------------------------------------------------------------

Generator函式返回的是一個遍歷器物件。

為什麼,請看下面的程式碼: 

    var g = gen()  

    g[Symbol.iterator] () ===g

MDN-Symbol.iterator   and  Ruanyf-ES6-Guide Iterator

----------------------------------------------------------------------------------

我們從一個例子開始:


function * gen () { console. log( 'started') console. log( yield 1) yield 2 yield 3 }


    可以這樣理解yield只是一個暫停狀態的標記。

    我們先獲取這個遍歷物件,var g = gen() 

     g.next()第一次函式開始執行,遇到yield所在的行就停止了,所以這一次是輸出了一個'started'. 但是我們知道.next()返回一個物件,物件的value 就是第一個暫停標記的yiled 後面表示式的值。 這一步的g.next()應該是{ value: 1, done: false }

     g.next()第二次執行,我們傳遞一個引數,g.next('abc') ,這時候我們先不討論引數,只是討論暫停狀態的標記是yield 2, 這時候執行的程式碼是 console.log(yield 1), 但是這裡我們傳遞了一個引數, 這個引數是上一個暫停標記(yield 表示式,請注意是整個yield表示式,也就是yield+expression)的值。所以這個yield 1 最後的值是'abc', 這時候輸出的是一個 'abc'。 這裡其實有點繞,實際上就是next(value),  這個value是上一個暫停表達式的值。

----------------------------------------------------------------------------------

    前面我們說了Generator是ES6的一個非同步解決方案,但是我們看了前面的例子,都是以同步的語法和書序來執行完。到底generator是怎麼幫助我們來處理非同步方法呢?

    沒錯,就是通過next(),next()方法可以傳遞一個引數,這個引數就是上一個yield 表示式的值。



function * gen ( a) { var x = yield a + 1 var y = yield x * 2 return y }

    var g = gen(3)

    我們執行幾次的結果是這樣的:

        g.next()  // {value: 4, done: false}              

        g.next() // {value: NaN, done: false}  這一步因為我們沒有給上一個 (yield 表示式)賦值,如果我們這樣寫,g.next(5),這樣上一個 yield a+1 的值就是 5,x的值就為5, 這一步.next()的value就是5*2為0。 但是這個引數我們是不能寫死的,因為這樣沒有任何的實際意義。我們可以將上一次的next()的value給儲存下來,然後作為引數傳遞給下個next()。

    我們來寫一個簡要的流程控制器,專門處理這一串流程:


function exceuteGen( generatorFunc) { let g = generatorFunc let value while( true) { var temp = g. next( value) if (! temp. done) { value = temp. value } else { return value break } }

}

var g = gen( 3) console. log( '最後的結果是 ' + exceuteGen( g))

    我們來看gen, 裡面y是依賴x的,我們以同步的方式去書寫這兩段程式碼,最後用一個執行器就完成了非同步操作,如果不去寫執行器,是不是覺得這樣寫比callback 和 promise都好的多,個人覺得是的。 callback一堆回撥,promise一堆then的情況都比較繁瑣,用generator要簡潔的多【但是好多場景是generator 和 promise搭配一起使用】。

----------------------------------------------------------------------------------

    上面我們舉得例子是序列,但是有的情況是需要並行的,比如我們要處理一個彙總資料,需要得到學生資料 和 書籍資料,兩個對應方法是 getStudents() 和 getBooks()。 同時去獲取這兩個資料,然後基於這兩個資料去執行下一步驟,我們可以這樣寫:


function* parallelTask () { let [ students, books] = yield [ getStudents(), getBooks() ] console. log( students, books) }

    

    //執行器的第二個next傳遞的引數格式請注意,要可迭代。傳入陣列或者Set【map請你自己去嘗試】,不然會報錯:(intermediate value) is not iterable

----------------------------------------------------------------------------------

    用Generator處理非同步的好處是什麼,通過上面的例子我們也能夠看出來,是以看起來同步的方式去書寫程式碼,然後用一個執行器去隱藏了非同步的一些資料傳遞。這樣我們以更加自然的方式去書寫我們的程式碼。

    一般我們都會用一些庫比如co,它們幫助我們寫好了執行器,我們只需要定義這個generator函式即可。

    co模組返回一個promise物件。

    檢視co的原始碼,其實可以發現,原理和我們上面寫的generator函式執行器是一樣的,我理解就是遞迴(可能不準確),不過co模組做了其他的優化。

    遞迴實現執行器的基本因素是什麼?就是每一次傳遞的引數個數是一樣的,我們預設回撥裡面傳給下一個回撥的引數是一個[next()的引數預設一個]這樣我們寫執行器的時候,就直接傳遞了,但是如果我們的回撥引數不只一個,多個巢狀回撥,甚至回撥引數個數不是規律遞增遞減,這樣的邏輯我們根本不能寫一個自動執行器。所以co模組將傳入的引數預設轉換成promise物件,promise的fullfilled callback function只能接受一個引數,我想這就是為co模組編寫自動執行器的一個前提條件



----------------------------------------------------------------------------------


async 和 generator類似,是為了非同步操作變得更加簡單,


var asyncReadFile = async function() { var f1 = await readFile( '/etc/fstab') var f2 = await readFile( '/etc/shells') console. log( f1. toString()) console. log( f2. toString()) }


和generator比起來好處在於,async函式內建了執行器,我們不用像generator一樣,額外的要附加一個執行器, async函式返回的是一個promise物件。



參考資料: generator -ruanyf 

                https://davidwalsh.name/es6-generators

                co -source code