快速回顧
在第三篇文章中,我們着重講解了生成器的基本行爲。你可能對此感到陌生,但是並不難理解。生成器函數與普通函數有很多相似之處,它們之間最大的不同是,普通函數一次執行完畢,而生成器函數體每次執行一部分,每當執行到一個yield表達式的時候就會暫停。
儘管在那篇文章中我們進行過詳細解釋,但我們始終未把所有特性結合起來給大家講解示例。現在就讓我們出發吧!
function* somewords() {
yield "hello";
yield "world";
}
for (var word of somewords()) {
alert(word);
}
這段腳本簡單易懂,但是如果你把代碼中不同的比特位當做戲劇中的任務,你會發現它變得如此與衆不同。穿上新衣的代碼看起來是這樣的:
(譯者注:下面這是原作者創作的一個劇本,他將ES6中的各種函數和語法擬人化,以講解生成器(Generator)的實現原理)
場景 - 另一個世界的計算機,白天
for loop女士獨自站在舞臺上,戴着一頂安全帽,手裏拿着一個筆記板,上面記載着所有的事情。
for loop:
(電話響起)
somewords()!
generator出現:這是一位高大的、有着一絲不苟紳士外表的黃銅機器人。
它看起來足夠友善,但給人的感覺仍然是冷冰冰的金屬。
for loop:
(瀟灑地拍了拍她的手)
好吧!我們去找些事兒做吧。
(對generator說)
.next()!
generator動了起來,就像突然擁有了生命。
generator:
{value: "hello", done: false}
然而猝不及防的,它以一個滑稽的姿勢停止了動作。
for loop:
alert!
alert小子飛快衝進舞臺,眼睛大睜,上氣不接下氣。我們感覺的到他一向如此。
for loop:
對user說“hello”。
alert小子轉身衝下舞臺。
alert:
(舞臺下,大聲尖叫)
一切都靜止了!
你正在訪問的頁面說,
“hello”!
停留了幾秒鐘後,alert小子跑回舞臺,穿過所有人滑停在for loop女士身邊。
alert:
user說ok。
for loop:
(瀟灑地拍了拍她的手)
好吧!我們去找些事兒做吧。
(回到generator身邊)
.next()!
generator又一次煥發生機。
generator:
{value: "world", done: false}
它換了個姿勢又一次凍結。
for loop:
alert!
alert:
(已經跑起來)
正在搞定!
(舞臺下,大聲尖叫)
一切都靜止了!
你正在訪問的頁面說,
“world”!
又一次暫停,然後alert突然跋涉回到舞臺,垂頭喪氣的。
alert:
user再一次說ok,但是…
但是請阻止這個頁面
創建額外的對話。
他噘着嘴離開了。
for loop:
(瀟灑地拍了拍她的手)
好吧!我們去找些事兒做吧。
(回到generator身邊)
.next()!
generator第三次煥發生機。
generator:
(莊嚴的)
{value: undefined, done: true}
它的頭低下了,光芒從它的眼裏消失。它不再移動。
for loop
我的午餐時間到了。
她離開了。
一會兒,garbage collector(垃圾收集器)老頭進入,撿起了奄奄一息的generator,將它帶下舞臺。
好吧,這一齣戲不太像哈姆雷特,但你應該可以想象得出來。
好吧,這一齣戲不太像哈姆雷特,但你應該可以想象得出來。
正如你在戲劇中看到的,當生成器對象第一次出現時,它立即暫停了。每當調用它的.next()
方法,它都會甦醒並向前執行一部分。
所有動作都是單線程同步的。請注意,無論何時永遠只有一個真正活動的角色,角色們不會互相打斷,亦不會互相討論,他們輪流講話,只要他們的話沒有說完都可以繼續說下去。(就像莎士比亞一樣!)
每當for-of
循環遍歷生成器時,這齣戲的某個版本就展開了。這些.next()
方法調用序列永遠不會在你的代碼的任何角落出現,在劇本里我把它們都放在舞臺上了,但是對於你和你的程序而言,所有這一切都應該在幕後完成,因爲生成器和for-of
循環就是被設計成通過迭代器接口聯結工作的。
所以,總結一下到目前爲止所有的一切:
- 生成器對象是可以產生值的優雅的黃銅機器人。
- 每個生成器函數體構成的單一代碼塊就是一個機器人。
如何關停生成器
我在第1部分沒有提到這些繁瑣的生成器特性:
- generator.return()
- generator.next()的可選參數
- generator.throw(error)
- yield*
如果你不理解這些特性存在得意義,就很難對它們提起興趣,更不用說理解它們的實現細節,所以我選擇直接跳過。但是當我們深入學習生成器時,勢必要仔細瞭解這些特性的方方面面。
你或許曾使用過這樣的模式:
function dothings() {
setup();
try {
// ... 做一些事情
} finally {
cleanup();
}
}
dothings();
清理(cleanup)過程包括關閉連接或文件,釋放系統資源,或者只是更新dom來關閉“運行中”的加載動畫。我們希望無論任務成功完成與否都觸發清理操作,所以執行流入到finally
代碼塊。
那麼生成器中的清理操作看起來是什麼樣的呢?
function* producevalues() {
setup();
try {
// ... 生成一些值
} finally {
cleanup();
}
}
for (var value of producevalues()) {
work(value);
}
這段代碼看起來很好,但是這裏有一個問題:我們沒在try
代碼塊中調用work(value)
,如果它拋出異常,我們的清理步驟會如何執行呢?
或者假設for-of
循環包含一條break
語句或return
語句。清理步驟又會如何執行呢?
放心,清理步驟無論如何都會執行,ES6已經爲你做好了一切。
我們第一次討論迭代器和for-of循環時曾說過,迭代器接口支持一個可選的.return()
方法,每當迭代在迭代器返回{done:true}
之前退出都會自動調用這個方法。生成器支持這個方法,mygenerator.return()
會觸發生成器執行任一finally
代碼塊然後退出,就好像當前的生成暫停點已經被祕密轉換爲一條return
語句一樣。
注意,.return()
方法並不是在所有的上下文中都會被自動調用,只有當使用了迭代協議的情況下才會觸發該機制。所以也有可能生成器沒執行finally
代碼塊就直接被垃圾回收了。
如何在舞臺上模擬這些特性?生成器被凍結在一個需要一些配置的任務(例如,建造一幢摩天大樓)中間。突然有人拋出一個錯誤!for
循環捕捉到這個錯誤並將它放置在一遍,她告訴生成器執行.return()
方法。生成器冷靜地拆除了所有腳手架並停工。然後for
循環取回錯誤,繼續執行正常的異常處理過程。
生成器主導模式
到目前爲止,我們在劇本中看到的生成器(generator)和使用者(user)之間的對話非常有限,現在換一種方式繼續解釋:
在這裏使用者主導一切流程,生成器根據需要完成它的任務,但這不是使用生成器進行編程的唯一方式。
在第1部分中我曾經說過,生成器可以用來實現異步編程,完成你用異步回調或promise鏈所做的一切。我知道你一定想知道它是如何實現的,爲什麼yield的能力(這可是生成器專屬的特殊能力)足夠應對這些任務。畢竟,異步代碼不僅產生(yield)數據,還會觸發事件,比如從文件或數據庫中調用數據,向服務器發起請求並返回事件循環來等待異步過程結束。生成器如何實現這一切?它又是如何不借助回調力量從文件、數據庫或服務器中接受數據?
爲了開始找出答案,考慮一下如果.next()
的調用者只有一種方法可以傳值返回給生成器會發生什麼?僅僅是這一點改變,我們就可能創造一種全新的會話形式:
事實上,生成器的.next()
方法接受一個可選參數,參數稍後會作爲yield
表達式的返回值出現在生成器中。那就是說,yield
語句與return
語句不同,它是一個只有當生成器恢復時纔會有值的表達式。
var results = yield getdataandlatte(request.areacode);
這一行代碼完成了許多功能:
- 調用
getdataandlatte()
,假設函數返回我們在截圖中看到的字符串“get me the database records for area code...
”。 - 暫停生成器,生成字符串值。
- 此時可以暫停任意長的時間。
- 最終,直到有人調用
.next({data: ..., coffee: ...})
,我們將這個對象存儲在本地變量results
中並繼續執行下一行代碼。
下面這段代碼完整地展示了這一行代碼完整的上下文會話:
function* handle(request) {
var results = yield getdataandlatte(request.areacode);
results.coffee.drink();
var target = mosturgentrecord(results.data);
yield updatestatus(target.id, "ready");
}
yield
仍然保持着它的原始含義:暫停生成器,返回值給調用者。但是確實也發生了變化!這裏的生成器期待來自調用者的非常具體的支持行爲,就好像調用者是它的行政助理一樣。
普通函數則與之不同,通常更傾向於滿足調用者的需求。但是你可以藉助生成器創造一段對話,拓展生成器與其調用者之間可能存在的關係。
這個行政助理生成器運行器可能是什麼樣的?它大可不必很複雜,就像這樣:
function rungeneratoronce(g, result) {
var status = g.next(result);
if (status.done) {
return; // phew!
}
// 生成器請我們去獲取一些東西並且
// 當我們搞定的時候再回調它
doasynchronousworkincludingespressomachineoperations(
status.value,
(error, nextresult) => rungeneratoronce(g, nextresult));
}
爲了讓這段代碼運行起來,我們必須創建一個生成器並且運行一次,像這樣:
rungeneratoronce(handle(request), undefined);
在之前的文章中,我一個庫的示例中提到Q.async()
,在那個庫中,生成器是可以根據需要自動運行的異步過程。rungeneratoronce
正式這樣的一個具體實現。事實上,生成器一般會生成Promise對象來告訴調用者要做的事情,而不是生成字符串來大聲告訴他們。
如果你已經理解了Promise的概念,現在又理解了生成器的概念,你可以嘗試修改rungeneratoronce
的代碼來支持Promise。這個任務不簡單,但是一旦成功,你將能夠用Promise線性書寫複雜的異步算法,而不僅僅通過.then()
方法或回調函數來實現異步功能。
如何銷燬生成器
你是否有看到rungeneratoronce
的錯誤處理過程?答案一定是沒有,因爲上面的示例中直接忽略了錯誤!
是的,那樣做不好,但是如果我們想要以某種方法給生成器報告錯誤,可以嘗試一下這個方法:當有錯誤產生時,不要繼續調用generator.next(result)
方法,而應該調用generator.throw(error)
方法來拋出yield
表達式,進而像.return()
方法一樣終止生成器的執行。但是如果當前的生成暫停點在一個try
代碼塊中,那麼會catch
到錯誤並執行finally
代碼塊,生成器就恢復執行了。
另一項艱鉅的任務來啦,你需要修改rungeneratoronce
來確保.throw()
方法能夠被恰當地調用。請記住,生成器內部拋出的異常總是會傳播到調用者。所以無論生成器是否捕獲錯誤,generator.throw(error)
都會拋出error
並立即返回給你。
當生成器執行到一個yield
表達式並暫停後可以實現以下功能:
- 調用
generator.next(value)
,生成器從離開的地方恢復執行。 - 調用
generator.return()
,傳遞一個可選值,生成器只執行finally
代碼塊並不再恢復執行。 - 調用
generator.throw(error)
,生成器表現得像是yield
表達式調用一個函數並拋出錯誤。 - 或者,什麼也不做,生成器永遠保持凍結狀態。(是的,對於一個生成器來說,很可能執行到一個
try
代碼塊,永不執行finally
代碼塊。這種狀態下的生成器可以被垃圾收集器回收。)
看起來生成器函數與普通函數的複雜度相當,只有.return()
方法顯得不太一樣。
事實上,yield
與函數調用有許多共通的地方。當你調用一個函數,你就暫時停止了,對不對?你調用的函數取得主導權,它可能返回值,可能拋出錯誤,或者永遠循環下去。
結合生成器實現更多功能
我再展示一個特性。假設我們寫一個簡單的生成器函數聯結兩個可迭代對象:
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
es6支持這樣的簡寫方式:
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
普通yield
表達式只生成一個值,而yield*
表達式可以通過迭代器進行迭代生成所有的值。
這個語法也可以用來解決另一個有趣的問題:在生成器中調用生成器。在普通函數中,我們可以從將一個函數重構爲另一個函數並保留所有行爲。很顯然我們也想重構生成器,但我們需要一種調用提取出來的子例程的方法,我們還需要確保,子例程能夠生成之前生成的每一個值。yield*
可以幫助我們實現這一目標。
function* factoredoutchunkofcode() { ... }
function* refactoredfunction() {
...
yield* factoredoutchunkofcode();
...
}
考慮一下這樣一個場景:一個黃銅機器人將子任務委託給另一個機器人,函數對組織同步代碼來說至關重要,所以這種思想可以使基於生成器特性的大型項目保持簡潔有序。