【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()。
我們來寫一個簡要的流程控制器,專門處理這一串流程:
}
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