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