英文原文: Replacing callbacks with ES6 Generators
目前,已經(jīng)有很多文章討論過(guò)了如何使用ES6 generators來(lái)取代JavaScript中經(jīng)常遇到的回調(diào)金字塔。但是,其中提到的絕大多數(shù)方法都需要依賴于某個(gè)庫(kù),而對(duì)于其中的原理卻提及甚少。
在本文中,我們將一步一步的將一個(gè)基于回調(diào)函數(shù)的例子修改為一個(gè)基于generator的例子。本文的目標(biāo)是讓你透徹地理解使用generator替代回調(diào)函數(shù)的原理。
Generator是JavaScript中一個(gè)新概念,但在編程語(yǔ)言中已經(jīng)存在已久。你可能已經(jīng)在其他的編程語(yǔ)言例如Python使用過(guò)它。如果沒(méi)有,也不要害怕,我們?cè)诤竺嬉呀?jīng)為你準(zhǔn)備了一個(gè)簡(jiǎn)單明了的入門(mén)介紹。
在我們開(kāi)始之前,你需要安裝Node 0.11.*
來(lái)運(yùn)行文章中的例子。當(dāng)你在運(yùn)行這些例子時(shí),你需要告訴Node使用ES6(也就是Harmony)來(lái)運(yùn)行:node -harmony example.js
。
在我們深入講述如何使用generator替代回調(diào)函數(shù)之前,我們先來(lái)說(shuō)說(shuō)什么是generator。
Generator很像是一個(gè)函數(shù),但是你可以暫停它的執(zhí)行。你可以向它請(qǐng)求一個(gè)值,于是它為你提供了一個(gè)值,但是余下的函數(shù)不會(huì)自動(dòng)向下執(zhí)行直到你再次向它請(qǐng)求一個(gè)值。
取號(hào)機(jī)也許是對(duì)generator的一個(gè)絕佳的比喻。你可以通過(guò)取一張票來(lái)向機(jī)器請(qǐng)求一個(gè)號(hào)碼。你接收了你的號(hào)碼,但是機(jī)器不會(huì)自動(dòng)為你提供下一個(gè)。換句話說(shuō),取票機(jī)暫停直到有人請(qǐng)求另一個(gè)號(hào)碼,此時(shí)它才會(huì)向后運(yùn)行。
Generator在ES6中像一個(gè)函數(shù)一樣被聲明,除了在之前有一個(gè)*
的差別外:
function* ticketGenerator() {
}
當(dāng)你想要一個(gè)generator提供一個(gè)值然后暫停時(shí),你需要使用yield
關(guān)鍵字。yield
有點(diǎn)像是return
關(guān)鍵字,因?yàn)樗鼈兌挤祷匾粋€(gè)值,但是函數(shù)在yield
之后會(huì)進(jìn)入暫停狀態(tài)。
function* ticketGenerator() {
yield 1;
yield 2;
yield 3;
}
在我們的例子中,我們定義了一個(gè)叫做ticketGenerator
的迭代器。如果你向它請(qǐng)求一個(gè)值,它會(huì)返回1然后暫停。如果你再次向它發(fā)出一個(gè)請(qǐng)求,我們將得到2,然后是3。
當(dāng)你調(diào)用一個(gè)generator時(shí),它將返回一個(gè)迭代器對(duì)象。這個(gè)迭代器對(duì)象擁有一個(gè)叫做next
的方法來(lái)幫助你重啟generator函數(shù)并得到下一個(gè)值。
next
方法不僅返回值,它返回的對(duì)象具有兩個(gè)屬性:done
和value
。value
是你獲得的值,done
用來(lái)表明你的generator是否已經(jīng)停止提供值。
現(xiàn)在我們從我們的取號(hào)機(jī)中取一些號(hào)碼,
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 }
現(xiàn)在我們的取號(hào)系統(tǒng)只能提供最多到3的號(hào)碼,這實(shí)在是沒(méi)什么用。我們想要讓它無(wú)線增加下去,因此我們來(lái)創(chuàng)建一個(gè)循環(huán)。
function* ticketGenerator() {
for(var i=0; true; i++) {
yield i;
}
}
現(xiàn)在,如果這是一個(gè)普通的函數(shù),我們每次只會(huì)得到0。但是使用generator卻不一樣:
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
每一次當(dāng)我們調(diào)用next()
時(shí),generator執(zhí)行下一個(gè)循環(huán)迭代然后暫停。這意味著我們擁有一個(gè)可以無(wú)限向下運(yùn)行的generator。因?yàn)檫@個(gè)generator只是發(fā)生了暫停,你并沒(méi)有凍結(jié)你的程序。事實(shí)上,generator是一個(gè)創(chuàng)建無(wú)限循環(huán)的好方法。
進(jìn)一步探究迭代迭代generator對(duì)象的話,next()
實(shí)際上還有另一個(gè)用途。如果你給next
傳遞一個(gè)值,它會(huì)被視為generator中的一個(gè)yield
語(yǔ)句的結(jié)果來(lái)對(duì)待。
因此next
是一個(gè)在generator運(yùn)行過(guò)程中向其傳遞信息的方式。我們將以此來(lái)修改我們的取號(hào)generator以便它能夠被重置到0。我們希望能在任何時(shí)間點(diǎn)來(lái)重置取號(hào)機(jī)。
function* ticketGenerator() {
for(var i=0; true; i++) {
var reset = yield i;
if(reset) {
i = -1;
}
}
}
正如你所看到的,如果yield
返回了一個(gè)true
然后我們將i
設(shè)置為-1
。那么for
循環(huán)將會(huì)在循環(huán)的結(jié)尾將i
增加1
,因此下一次返回的i
變成了0
。
我們來(lái)看看實(shí)際情況如何:
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
既然我們已經(jīng)學(xué)到了一些關(guān)于generator的知識(shí),現(xiàn)在讓我們來(lái)談?wù)刧enerator和回調(diào)函數(shù)。正如你所知道的,當(dāng)我們調(diào)用例如AJAX請(qǐng)求這樣的異步代碼時(shí)我們會(huì)使用回調(diào)函數(shù)。為了簡(jiǎn)單起見(jiàn)我們?cè)诶又卸x一個(gè)delay函數(shù)。
我們的delay函數(shù)將會(huì)是異步的 – 在指定的時(shí)間過(guò)后我們提供給delay的回調(diào)函數(shù)才會(huì)被執(zhí)行,然后delay會(huì)給你的回調(diào)函數(shù)傳遞一個(gè)字符串告訴它究竟沉睡了多久。
在此期間你的其余代碼將會(huì)繼續(xù)執(zhí)行下去。這就好像是進(jìn)行一個(gè)AJAX請(qǐng)求一樣 – 你發(fā)出請(qǐng)求,你的代碼繼續(xù)執(zhí)行,當(dāng)服務(wù)器返回一個(gè)結(jié)果時(shí)你的回調(diào)函數(shù)才執(zhí)行。
現(xiàn)在,我們來(lái)定義delay函數(shù),
function delay(time, callback){
setTimeout(function(){
callback("Slept for "+time);
},time);
}
到目前為止,還沒(méi)有什么特別的東西?,F(xiàn)在我們來(lái)使用它來(lái)delay兩次。首先我們將delay 1000ms,然后當(dāng)delay結(jié)束后我們?cè)倭硗?em>delay 1200ms。
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
確保我們的兩個(gè)delay依次被調(diào)用的唯一方法就是確保第二個(gè)delay在第一個(gè)delay的回調(diào)函數(shù)中。
如果我們要依次delay 12次,我們將需要嵌套的調(diào)用12次delay函數(shù)。這時(shí)你就會(huì)碰到回調(diào)金字塔,代碼也變得丑陋不堪。
Generator是解決回調(diào)地獄的有效方法之一。異步調(diào)用是很困難的事情,因?yàn)槲覀兊暮瘮?shù)不會(huì)等待異步調(diào)用完成,因此我們需要回調(diào)函數(shù)。
使用generator,我們可以讓我們的代碼進(jìn)行等待。無(wú)需嵌套回調(diào)函數(shù),我們可以使用generator確保當(dāng)異步調(diào)用在我們的generator函數(shù)運(yùn)行一下行代碼之前完成時(shí)暫停函數(shù)的執(zhí)行。
因此,如果我們可以在一個(gè)異步調(diào)用完成時(shí)暫停執(zhí)行,這就意味著我們可以依次調(diào)用delay函數(shù) – 就像delay函數(shù)是同步執(zhí)行的一樣。
首先,我們知道我們進(jìn)行異步調(diào)用的代碼需要在一個(gè)generator而不是一個(gè)一般的函數(shù)中進(jìn)行,因此我們來(lái)定義一個(gè)generator,
function* myDelayedMessages() {
/* delay 1000 ms and print the result */
/* delay 1200 ms and print the result */
}
接下來(lái)我們需要在我們的generator中調(diào)用delay。記住,delay接收一個(gè)回調(diào)函數(shù)。這個(gè)回調(diào)函數(shù)需要繼續(xù)我們的generator,但是我們現(xiàn)在還沒(méi)有一個(gè)generator因此我們先放上一個(gè)空函數(shù)。
function* myDelayedMessages() {
console.log(delay(1000, function(){}));
console.log(delay(1200, function(){}));
}
我們代碼依然是異步的。這是因?yàn)槲覀冞€沒(méi)有將放入任何的yield語(yǔ)句。Generator只是在它們看大一個(gè)yield語(yǔ)句時(shí)才暫停。
function* myDelayedMessages() {
console.log(yield delay(1000, function(){}));
console.log(yield delay(1200, function(){}));
}
我們現(xiàn)在已經(jīng)更接近了一點(diǎn)了。然而,如果我們運(yùn)行我們的generator什么也不會(huì)發(fā)生。因?yàn)闆](méi)有什么東西告訴它要向下運(yùn)行。
在這里你需要理解的最重要的概念是:generator需要在delay中的回調(diào)函數(shù)運(yùn)行完成后繼續(xù)往下運(yùn)行,這就是它們?nèi)绾沃罆和?yīng)該結(jié)束了的原因。
這意味著回調(diào)函數(shù)中的東西需要知道如何向前推動(dòng)generator。我們?cè)谄渲袀鬟f一個(gè)叫做resume
的函數(shù)來(lái)為我們做這件事。記住我們現(xiàn)在還依然沒(méi)有定義resume
。
function* myDelayedMessages(resume) {
console.log(yield delay(1000, resume));
console.log(yield delay(1200, resume));
}
OK,現(xiàn)在我們的generator將會(huì)接收一個(gè)resume函數(shù),這個(gè)函數(shù)將會(huì)向前推動(dòng)generator。
現(xiàn)在到了關(guān)鍵步驟了,我們?nèi)绾蝸?lái)編寫(xiě)resume
,它又是怎么來(lái)了解我們的generator的。
如果你看看其他使用generator代替回調(diào)函數(shù)的例子,你會(huì)看到generator函數(shù)總是被另一個(gè)函數(shù)包裹著 – 通常是一個(gè)叫做run
或者execute
的函數(shù)。這些函數(shù)的目的有以下幾個(gè):
resume
函數(shù)來(lái)使用這個(gè)generator迭代器對(duì)象來(lái)推進(jìn)generatorresume
函數(shù)傳遞給這個(gè)generator以便generator能夠訪問(wèn)resumeyield
之前開(kāi)始執(zhí)行現(xiàn)在我們來(lái)創(chuàng)建run
函數(shù),
function run(generatorFunction) {
var generatorItr = generatorFunction(resume);
function resume(callbackValue) {
generatorItr.next(callbackValue);
}
generatorItr.next()
}
現(xiàn)在我們有了一個(gè)能夠接收一個(gè)generator函數(shù)的函數(shù),并為它傳遞了一個(gè)了解如何推進(jìn)generator迭代器對(duì)象的函數(shù)。
注意到我們?cè)?code>resume函數(shù)中用到了next
的第二個(gè)特性。resume
是被傳遞給delay的回調(diào)函數(shù),因此它接收delay函數(shù)提供的值。resume將這個(gè)值傳遞給next,因此yield語(yǔ)句的結(jié)果實(shí)際上是我們異步函數(shù)的結(jié)果!
我們現(xiàn)在要做的只是用run
包裹上我們的generator函數(shù),然后我們就能看到以下結(jié)果,
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"
現(xiàn)在,你能看到我們調(diào)用delay
兩次,并沒(méi)有使用嵌套回調(diào)函數(shù)。如果你依然看到疑惑,我們現(xiàn)在概括的來(lái)講述以下究竟發(fā)生了什么:
run
接收了我們的generator并創(chuàng)建了一個(gè)resume函數(shù)run
創(chuàng)建了一個(gè)generator迭代器對(duì)象(我們?cè)谒厦嬲{(diào)用next
方法),提供了resume
函數(shù)。接著它推動(dòng)了generator迭代器向前運(yùn)行。delay
在1000ms之后完成然后調(diào)用resume
。resume
告訴我們的generator進(jìn)行下一步。它將結(jié)果傳遞給delay以便consol
e能夠?qū)⑺蛴〕鰜?lái)。yield
,調(diào)用delay
然后再次暫停。 delay
等待1200ms之后調(diào)用resume
回調(diào)函數(shù)。resume
再次推進(jìn)generator。yield
的調(diào)用,這個(gè)generator完成執(zhí)行。我們已經(jīng)成功的使用generator替代了回調(diào)嵌套方法??偨Y(jié)一下,使用generator替代回調(diào)函數(shù)要包含以下幾個(gè)步驟:
run
函數(shù)來(lái)接受一個(gè)generator,并為這個(gè)generator提供resume函數(shù)。resume
函數(shù)來(lái)推進(jìn)generator,然后在resume
被異步函數(shù)調(diào)用時(shí)將這個(gè)resume
函數(shù)傳遞給generator。resume
作為回調(diào)傳遞給我們所有的異步回調(diào)函數(shù)。這些異步函數(shù)在完成時(shí)執(zhí)行resume
,這使得我們的generator在每個(gè)異步調(diào)用完成之時(shí)僅僅向前一步。雖然generator究竟是不是一個(gè)處理回調(diào)地獄的好方法還在討論之中,但是它確實(shí)是一個(gè)加強(qiáng)你對(duì)ES6中g(shù)enerator和迭代器理解的練習(xí)。如果你在尋找一些不需要用到ES6的處理嵌套回調(diào)函數(shù)的方法,可以考慮promises
。
更多建議: