what is generator
Generators算得上js的一個新概念函數。它看起來像是一個函數,但是可以暫停執行。從語法上看,有3點關注:
函數名前有一個*
函數內有yield
調用時返回一個對象,對這個對象調用next才會繼續執行
你的node支持generator了嗎?
在node 0.11以上,對node必須加入–harmony 參數,即可支持:
JavaScript
$ node --harmony
> function *a(){}
undefined
>
看到undefined就說明支持了。如果不加參數,默認不支持。你會看到
JavaScript
$ node
> function *a(){}
...
聲明一個generator 是這樣的:
JavaScript
function* ticketGenerator() {}
如果想要 generator 提供一個值并暫停,那么需要使用yeild 關鍵字。yield 就像 return 一樣返回一個值。和它不同的是,yield會暫停函數。
JavaScript
function* ticketGenerator() {
yield 1;
yield 2;
yield 3;
}
我們做了一個迭代器,叫做 ticketGenerator. 我們可以和它要一個值,然后它返回1,然后暫停。依次返回2,3:
JavaScript
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 ,不太有用.我們可以遞增到無窮。無窮數列來了。
JavaScript
function* ticketGenerator() {
for(var i=0; true; i++) {
yield i;
}
}
再來一遍:
JavaScript
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 語句的返回值。我們可以利用這個特性,把剛剛的無限數列做一次重置:
JavaScript
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提供的值(如果提供了的話)。
看看效果:
JavaScript
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函數,延遲一些時間,然后打印點文字:
JavaScript
function delay(time, callback) {
setTimeout(function () {
callback("Slept for "+time);
}, time);
}
調用它也是常見代碼:
JavaScript
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 ,而不是典型的函數,所以加個星在函數前:
JavaScript
function* myDelayedMessages() {
/* delay 1000 ms and print the result */
/* delay 1200 ms and print the result */
}
加入delay調用。delay需要callback。這個callback需要調用generator.next(),以便繼續代碼。我們先放一個空的callback:
JavaScript
function* myDelayedMessages() {
console.log(delay(1000, function(){}));
console.log(delay(1200, function(){}));
}
現在代碼依然是異步的。加入yield:
JavaScript
function* myDelayedMessages() {
console.log(yield delay(1000, function(){}));
console.log(yield delay(1200, function(){}));
}
又近了一步。但是需要有人告訴generator向前走,走起來。
關鍵概念在這里:當delay完成時,需要在它的callback內執行點東西,這些東西讓generator向前走(調用next)
這個函數,且不管如何實現,我們知道它得叫做resume:
JavaScript
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就可以滾動起來了:
JavaScript
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打印出來。就像“倒脫靴”。
代碼整合起來:
JavaScript
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為例:
JavaScript
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),作為常見的異步函數如何修改?
JavaScript
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,就可以:
JavaScript
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+個,要不要獨立出去:):
打個總結
成功。我們用generator替換了callback。我們這樣做到的:
創建一個run函數,以 generator 為參數, 并傳遞一個 resume 函數給它
傳遞resume 函數,單步推動generator ,返回任何異步callback獲得的值給run
傳遞resume 作為異步調用callback . 這些異步函數一旦完成,就調用callback,因此就是調用resume。允許resume推動generator
generators替代“callback hell” 是否佳是可爭論的,但是這個練習可以幫助你理解到ES6的generators 和iterators
深圳網站建設www.ykfic.cn