what is generator
Generators算得上js的一個新概念函式。它看起來像是一個函式,但是可以暫停執行。從語法上看,有3點關注:
- 函式名前有一個*
- 函式內有yield
- 呼叫時返回一個物件,對這個物件呼叫next才會繼續執行
你的node支援generator了嗎?
在node 0.11以上,對node必須加入–harmony 引數,即可支援:
1 2 3 4 |
$ node --harmony > function *a(){} undefined > |
看到undefined就說明支援了。如果不加引數,預設不支援。你會看到
1 2 3 |
$ node > function *a(){} ... |
Generators in ES6
宣告一個generator 是這樣的:
1 |
function* ticketGenerator() {} |
如果想要 generator 提供一個值並暫停,那麼需要使用yeild 關鍵字。yield 就像 return 一樣返回一個值。和它不同的是,yield會暫停函式。
1 2 3 4 5 |
function* ticketGenerator() { yield 1; yield 2; yield 3; } |
我們做了一個迭代器,叫做 ticketGenerator. 我們可以和它要一個值,然後它返回1,然後暫停。依次返回2,3:
1 2 3 4 5 6 7 8 9 |
var takeANumber = ticketGenerator(); takeANumber.next(); // > { value: 1, done: false } takeANumber.next(); // > { value: 2, done: false } takeANumber.next(); // > { value: 3, done: false } takeANumber.next(); // > { value: undefined, done: true } |
現在我們返回最大為3 ,不太有用.我們可以遞增到無窮。無窮數列來了。
1 2 3 4 5 |
function* ticketGenerator() { for(var i=0; true; i++) { yield i; } } |
再來一遍:
1 2 3 4 5 6 |
var takeANumber = ticketGenerator(); console.log(takeANumber.next().value); //0 console.log(takeANumber.next().value); //1 console.log(takeANumber.next().value); //2 console.log(takeANumber.next().value); //3 console.log(takeANumber.next().value); //4 |
每次疊加,無窮列舉。這就有點意思了。
干預yield返回值
除了可以列舉累加外, next() 還有第二種用法:如果你傳遞一個值給next,它會作為yield 語句的返回值。我們可以利用這個特性,把剛剛的無限數列做一次重置:
1 2 3 4 5 6 |
function* ticketGenerator() { for(var i=0; true; i++) { var reset = yield i; if(reset) { i = -1; } } } |
這樣當我們呼叫next(true)的時候,i會等於-1,從而重設了i值到初始值。
無需困惑, yield i 會把i發到generator的呼叫next處,但是generator內部我們使用next提供的值(如果提供了的話)。
看看效果:
1 2 3 4 5 6 |
var takeANumber = ticketGenerator(); console.log(takeANumber.next().value); //0 console.log(takeANumber.next().value); //1 console.log(takeANumber.next().value); //2 console.log(takeANumber.next(true).value); //0 console.log(takeANumber.next().value); //1 |
這就是generator。古怪,有趣。接下來會繼續分析它的應用,在解決callback hell方面。
問題
拿一個案例開刀吧。我們來看一個delay函式,延遲一些時間,然後列印點文字:
1 2 3 4 5 |
function delay(time, callback) { setTimeout(function () { callback("Slept for "+time); }, time); } |
呼叫它也是常見程式碼:
1 2 3 4 5 6 7 8 9 10 |
delay(1000, function(msg) { console.log(msg); delay(1200, function (msg) { console.log(msg); } }) //...waits 1000ms // > "Slept for 1000" //...waits another 1200ms // > "Slept for 1200" |
為了保證兩次列印的次序,我們唯一的辦法,就是通過callback來完成。
要是延遲多幾次,比如12次,我們需要12次巢狀的callback,程式碼不段向右延伸。哇,回撥金字塔。
呼叫Generators
非同步是node的靈魂。可是非同步的麻煩在於callback,因為我餓每年需要等待完成通知,所以需要callback。
有了 generators,我們可以讓程式碼等。無需callback。可以用generators 在每個非同步呼叫進行中,在呼叫next()之前暫停執行。還記得yield/next 嗎?這是generators的絕活。
怎麼弄?
我們首先得知道,我們要把非同步呼叫暫停需要用到generator ,而不是典型的函式,所以加個星在函式前:
1 2 3 4 |
function* myDelayedMessages() { /* delay 1000 ms and print the result */ /* delay 1200 ms and print the result */ } |
加入delay呼叫。delay需要callback。這個callback需要呼叫generator.next(),以便繼續程式碼。我們先放一個空的callback:
1 2 3 4 |
function* myDelayedMessages() { console.log(delay(1000, function(){})); console.log(delay(1200, function(){})); } |
現在程式碼依然是非同步的。加入yield:
1 2 3 4 |
function* myDelayedMessages() { console.log(yield delay(1000, function(){})); console.log(yield delay(1200, function(){})); } |
又近了一步。但是需要有人告訴generator向前走,走起來。
關鍵概念在這裡:當delay完成時,需要在它的callback內執行點東西,這些東西讓generator向前走(呼叫next)
這個函式,且不管如何實現,我們知道它得叫做resume:
1 2 3 4 |
function* myDelayedMessages(resume) { console.log(yield delay(1000, resume)); console.log(yield delay(1200, resume)); } |
我們得把定義好的resume傳遞給 myDelayedMessages
變魔術了…
如何實現resume,它又如何知道我們的generator?
給generator 函式加個外套,外套函式的功能就是啟動generator,傳遞寫好的resume,第一次撥動generator(呼叫next),等待resume被呼叫,在resume內繼續撥動generator。這樣generator就可以滾動起來了:
1 2 3 4 5 6 7 |
function run(generatorFunction) { var generatorItr = generatorFunction(resume); function resume(callbackValue) { generatorItr.next(callbackValue); } generatorItr.next() } |
有點燒腦。當年寫作名噪一時的“goto statement considered harmful”的作者,看到此程式碼非得氣死。這裡沒有一行goto,卻跳來跳去的比有goto的還難。
注意哦,我們利用了next的第二個特性。resume 就是傳遞給callback 的函式,它因此接受了delay提供的值,resume傳遞這個值給 next, 故而 yield 語句返回了我們的非同步函式的結果。非同步函式的結果於是被console列印出來。就像“倒脫靴”。
程式碼整合起來:
1 2 3 4 5 6 7 8 |
run(function* myDelayedMessages(resume) { console.log(yield delay(1000, resume)); console.log(yield delay(1200, resume)); }) //...waits 1000ms // > "Slept for 1000" //...waits 1200ms // > "Slept for 1200" |
就是這樣。我們呼叫兩次delay,按照次序執行,卻沒有callback的巢狀。如果要呼叫12次,也還是沒有callback巢狀。如果你依然迷惑不解,我們再次分步來一遍》
- run 以generator 為引數,並且內部建立了一個resume 函式
- run 建立一個generator函式,傳遞resume給它
- 然後run第一次呼叫next,啟動了generator函式到yield
- generator 遇到了第一個yield,並呼叫yield後的delay,然後暫停。
- delay 在1000ms後完成,呼叫resume
- resume 告訴generator 在走一步。並且傳遞delay的結果給run函式,由console 列印
- generator 遇到第二個yield, 呼叫delay ,再次暫停
- delay 在1200ms後完成,呼叫resume
- resume 告訴generator 在走一步。並且傳遞delay的結果給run函式,由console 列印
co 更好
上面談到的做法,確實可以把非同步改成同步了。可是並不太完美:比如resume顯得比較突兀,在比如只能在callback返回一個值。不夠通用。
這樣的話,可以考慮TJ開發的co。連resume的宣告和引用也省掉了。還是以delay為例:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var co = require('co'); function delay(time) { return function (callback){ setTimeout(function () {// callback(null,"Slept for "+time); }, time); } } co(function *() { console.log(yield delay(1000)) console.log(yield delay(1200)) }) |
為了和co適配,delay需要做些修改,去掉callback,返回一個帶callback的函式,把計算結果通過callback傳遞出去。第一個引數依照node的規矩,留給err。
更絕。怪的不TJ被社群成為大神。
再來一個。readFile(file,callback),作為常見的非同步函式如何修改?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var co = require('co'); function readFile(file){ return function (callback ){ var fs = require("fs") fs.readFile(file,callback) } } co(function *() { console.log(yield readFile("./app.js")) }) //<Buffer 76 61 72 20 63 ... > |
可是,readFile改造這樣過工作,純粹就是boilerplate !所以,有人做了這樣的工作。安裝co-fs,就可以:
1 2 3 4 5 6 |
co(function *() { var fs = require("co-fs") var js = yield fs.readFile('./app1.js', 'utf8') var files = yield fs.readdir('.') console.log(js,files) }) |
node.js真是玩梯雲縱。以為已經很好了,還是有人在加入一把火。
所以,值得去npm看看,查詢下co-打頭的庫,有1000+個,要不要獨立出去:):
1 |
https://www.npmjs.com/search?q=co |
打個總結
成功。我們用generator替換了callback。我們這樣做到的:
- 建立一個run函式,以 generator 為引數, 並傳遞一個 resume 函式給它
- 傳遞resume 函式,單步推動generator ,返回任何非同步callback獲得的值給run
- 傳遞resume 作為非同步呼叫callback . 這些非同步函式一旦完成,就呼叫callback,因此就是呼叫resume。允許resume推動generator
generators替代“callback hell” 是否最佳是可爭論的,但是這個練習可以幫助你理解到ES6的generators 和iterators
原文:http://modernweb.com/2014/02/10/replacing-callbacks-with-es6-generator…
參考:
Harmony Generator, yield, ES6, co框架學習 – http://bg.biedalian.com/2013/12/21/harmony-generator.html
Koa, co and coruntine – Harmony – 前端亂燉 – http://www.html-js.com/article/1752
fork me
fork me from :https://github.com/1000copy/learningnode/blob/master/nodebook/generato…