第四章: Generator 1

特別說(shuō)明,為便于查閱,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS

在第二章中,我們發(fā)現(xiàn)了在使用回調(diào)表達(dá)異步流程控制時(shí)的兩個(gè)關(guān)鍵缺陷:

  • 基于回調(diào)的異步與我們的大腦規(guī)劃任務(wù)的各個(gè)步驟的過(guò)程不相符。
  • 由于 控制倒轉(zhuǎn) 回調(diào)是不可靠的,也是不可組合的。

在第三章中,我們?cè)敿?xì)地討論了Promise如何反轉(zhuǎn)回調(diào)的 控制倒轉(zhuǎn),重建了可靠性/可組合性。

現(xiàn)在讓我們把注意力集中到用一種順序的,看起來(lái)同步的風(fēng)格來(lái)表達(dá)異步流程控制。使這一切成為可能的“魔法”是ES6的 generator。

打破運(yùn)行至完成

在第一章中,我們講解了一個(gè)JS開(kāi)發(fā)者們?cè)谒麄兊拇a中幾乎永恒依仗的一個(gè)認(rèn)識(shí):一旦函數(shù)開(kāi)始執(zhí)行,它將運(yùn)行直至完成,沒(méi)有其他的代碼可以在運(yùn)行期間干擾它。

這看起來(lái)可能很滑稽,ES6引入了一種新型的函數(shù),它不按照“運(yùn)行至完成”的行為進(jìn)行動(dòng)作。這種新型的函數(shù)稱為“generator(生成器)”。

為了理解它的含義,讓我們看看這個(gè)例子:

var x = 1;

function foo() {
    x++;
    bar();              // <-- 這一行會(huì)發(fā)生什么?
    console.log( "x:", x );
}

function bar() {
    x++;
}

foo();                  // x: 3

在這個(gè)例子中,我們確信bar()會(huì)在x++console.log(x)之間運(yùn)行。但如果bar()不在這里呢?很明顯結(jié)果將是2而不是3

現(xiàn)在讓我們來(lái)燃燒你的大腦。要是bar()不存在,但以某種方式依然可以在x++console.log(x)語(yǔ)句之間運(yùn)行呢?這可能嗎?

搶占式(preemptive) 多線程語(yǔ)言中,bar()去“干擾”并正好在兩個(gè)語(yǔ)句之間那一時(shí)刻運(yùn)行,實(shí)質(zhì)上時(shí)可能的。但JS不是搶占式的,也(還)不是多線程的。但是,如果foo()本身可以用某種辦法在代碼的這一部分指示一個(gè)“暫?!保敲催@種“干擾”(并發(fā))的 協(xié)作 形式就是可能的。

注意: 我使用“協(xié)作”這個(gè)詞,不僅是因?yàn)樗c經(jīng)典的并發(fā)術(shù)語(yǔ)有關(guān)聯(lián)(見(jiàn)第一章),也因?yàn)檎缒銓⒃谙乱粋€(gè)代碼段中看到的,ES6在代碼中指示暫停點(diǎn)的語(yǔ)法是yield——暗示一個(gè)讓出控制權(quán)的禮貌的 協(xié)作

這就是實(shí)現(xiàn)這種協(xié)作并發(fā)的ES6代碼:

var x = 1;

function *foo() {
    x++;
    yield; // 暫停!
    console.log( "x:", x );
}

function bar() {
    x++;
}

注意: 你將很可能在大多數(shù)其他的JS文檔/代碼中看到,一個(gè)generator的聲明被格式化為function* foo() { .. }而不是我在這里使用的function *foo() { .. }——唯一的區(qū)別是擺放*位置的風(fēng)格。這兩種形式在功能性/語(yǔ)法上是完全一樣的,還有第三種function*foo() { .. }(沒(méi)空格)形式。這兩種風(fēng)格存在爭(zhēng)議,但我基本上偏好function *foo..,因?yàn)楫?dāng)我在寫作中用*foo()引用一個(gè)generator時(shí),這種形式可以匹配我寫的東西。如果我只說(shuō)foo(),你就不會(huì)清楚地知道我是在說(shuō)一個(gè)generator還是一個(gè)一般的函數(shù)。這純粹是一個(gè)風(fēng)格偏好的問(wèn)題。

現(xiàn)在,我們?cè)撊绾芜\(yùn)行上面的代碼,使bar()yield那一點(diǎn)取代*foo()的執(zhí)行?

// 構(gòu)建一個(gè)迭代器`it`來(lái)控制generator
var it = foo();

// 在這里開(kāi)始`foo()`!
it.next();
x;                      // 2
bar();
x;                      // 3
it.next();              // x: 3

好了,這兩段代碼中有不少新的,可能使人困惑的東西,所以我們得跋涉好一段了。在我們用ES6的generator來(lái)講解不同的機(jī)制/語(yǔ)法之前,讓我們過(guò)一遍這個(gè)行為的流程:

  1. it = foo()操作 不會(huì) 執(zhí)行*foo()generator,它只不過(guò)構(gòu)建了一個(gè)用來(lái)控制它執(zhí)行的 迭代器(iterator)。我們一會(huì)更多地討論 迭代器。
  2. 第一個(gè)it.next()啟動(dòng)了*foo()generator,并且運(yùn)行*foo()第一行上的x++。
  3. *foo()yield語(yǔ)句處暫停,就在這時(shí)第一個(gè)it.next()調(diào)用結(jié)束。在這個(gè)時(shí)刻,*foo()依然運(yùn)行而且是活動(dòng)的,但是處于暫停狀態(tài)。
  4. 我們觀察x的值,現(xiàn)在它是2.
  5. 我們調(diào)用bar(),它再一次用x++遞增x。
  6. 我們?cè)僖淮斡^察x的值,現(xiàn)在它是3。
  7. 最后的it.next()調(diào)用使*foo()generator從它暫停的地方繼續(xù)運(yùn)行,而后運(yùn)行使用x的當(dāng)前值3console.log(..)語(yǔ)句。

清楚的是,*foo()啟動(dòng)了,但 沒(méi)有 運(yùn)行到底——它停在yield。我們稍后繼續(xù)*foo(),讓它完成,但這甚至不是必須的。

所以,一個(gè)generator是一種函數(shù),它可以開(kāi)始和停止一次或多次,甚至沒(méi)必要一定要完成。雖然為什么它很強(qiáng)大看起來(lái)不那么明顯,但正如我們將要在本章剩下的部分將要講到的,它是我們用于在我們的代碼中構(gòu)建“generator異步流程控制”模式的基礎(chǔ)構(gòu)建塊兒之一。

輸入和輸出

一個(gè)generator函數(shù)是一種帶有我們剛才提到的新型處理模型的函數(shù)。但它仍然是一個(gè)函數(shù),這意味著依舊有一些不變的基本原則——即,它依然接收參數(shù)(也就是“輸入”),而且它依然返回一個(gè)值(也就是“輸出”):

function *foo(x,y) {
    return x * y;
}

var it = foo( 6, 7 );

var res = it.next();

res.value;      // 42

我們將67分別作為參數(shù)xy傳遞給*foo(..)。而*foo(..)將值42返回給調(diào)用端代碼。

現(xiàn)在我們可以看到發(fā)生器的調(diào)用和一般函數(shù)的調(diào)用的一個(gè)不同之處了。foo(6,7)顯然看起來(lái)很熟悉。但微妙的是,*foo(..)generator不會(huì)像一個(gè)函數(shù)那樣實(shí)際運(yùn)行起來(lái)。

相反,我們只是創(chuàng)建了 迭代器 對(duì)象,將它賦值給變量it,來(lái)控制*foo(..)generator。當(dāng)我們調(diào)用it.next()時(shí),它指示*foo(..)generator從現(xiàn)在的位置向前推進(jìn),直到下一個(gè)yield或者generator的最后。

next(..)調(diào)用的結(jié)果是一個(gè)帶有value屬性的對(duì)象,它持有從*foo(..)返回的任何值(如果有的話)。換句話說(shuō),yield導(dǎo)致在generator運(yùn)行期間,一個(gè)值被從中發(fā)送出來(lái),有點(diǎn)兒像一個(gè)中間的return。

但是,為什么我們需要這個(gè)完全間接的 迭代器 對(duì)象來(lái)控制generator還不清楚。我們回頭會(huì)討論它的,我保證。

迭代通信

generator除了接收參數(shù)和擁有返回值,它們還內(nèi)建有更強(qiáng)大,更吸引人的輸入/輸出消息能力,這是通過(guò)使用yieldnext(..)實(shí)現(xiàn)的。

考慮下面的代碼:

function *foo(x) {
    var y = x * (yield);
    return y;
}

var it = foo( 6 );

// 開(kāi)始`foo(..)`
it.next();

var res = it.next( 7 );

res.value;      // 42

首先,我們將6作為參數(shù)x傳入。之后我們調(diào)用it.next(),它啟動(dòng)了*foo(..).

*foo(..)內(nèi)部,var y = x ..語(yǔ)句開(kāi)始被處理,但它運(yùn)行到了一個(gè)yield表達(dá)式。就在這時(shí),它暫停了*foo(..)(就在賦值語(yǔ)句的中間?。?,而且請(qǐng)求調(diào)用端代碼為yield表達(dá)式提供一個(gè)結(jié)果值。接下來(lái),我們調(diào)用it.next(7),將7這個(gè)值傳回去作為暫停的yield表達(dá)式的結(jié)果。

所以,在這個(gè)時(shí)候,賦值語(yǔ)句實(shí)質(zhì)上是var y = 6 * 7。現(xiàn)在,return y將值42作為結(jié)果返回給it.next( 7 )調(diào)用。

注意一個(gè)非常重要,而且即便是對(duì)于老練的JS開(kāi)發(fā)者也非常容易犯糊涂的事情:根據(jù)你的角度,在yieldnext(..)調(diào)用之間存在著錯(cuò)位。一般來(lái)說(shuō),你所擁有的next(..)調(diào)用的數(shù)量,會(huì)比你所擁有的yield語(yǔ)句的數(shù)量多一個(gè)——前面的代碼段中有一個(gè)yield和兩個(gè)next(..)調(diào)用。

為什么會(huì)有這樣的錯(cuò)位?

因?yàn)榈谝粋€(gè)next(..)總是啟動(dòng)一個(gè)generator,然后運(yùn)行至第一個(gè)yield。但是第二個(gè)next(..)調(diào)用滿足了第一個(gè)暫停的yield表達(dá)式,而第三個(gè)next(..)將滿足第二個(gè)yield,如此反復(fù)。

兩個(gè)疑問(wèn)的故事

實(shí)際上,你主要考慮的是哪部分代碼會(huì)影響你是否感知到錯(cuò)位。

僅考慮generator代碼:

var y = x * (yield);
return y;

第一個(gè) yield基本上是在 問(wèn)一個(gè)問(wèn)題:“我應(yīng)該在這里插入什么值?”

誰(shuí)來(lái)回答這個(gè)問(wèn)題?好吧,第一個(gè) next()在這個(gè)時(shí)候已經(jīng)為了啟動(dòng)generator而運(yùn)行過(guò)了,所以很明顯 不能回答這個(gè)問(wèn)題。所以,第二個(gè) next(..)調(diào)用必須回答由 第一個(gè) yield提出的問(wèn)題。

看到錯(cuò)位了吧——第二個(gè)對(duì)第一個(gè)?

但是讓我們反轉(zhuǎn)一下我們的角度。讓我們不從generator的角度看問(wèn)題,而從迭代器的角度看。

為了恰當(dāng)?shù)孛枋鲞@種角度,我們還需要解釋一下,消息可以雙向發(fā)送——yield ..作為表達(dá)式可以發(fā)送消息來(lái)應(yīng)答next(..)調(diào)用,而next(..)可以發(fā)送值給暫停的yield表達(dá)式??紤]一下這段稍稍調(diào)整過(guò)的代碼:

function *foo(x) {
    var y = x * (yield "Hello");    // <-- 讓出一個(gè)值!
    return y;
}

var it = foo( 6 );

var res = it.next();    // 第一個(gè)`next()`,不傳遞任何東西
res.value;              // "Hello"

res = it.next( 7 );     // 傳遞`7`給等待中的`yield`
res.value;              // 42

yield ..next(..)一起成對(duì)地 在generator運(yùn)行期間 構(gòu)成了一個(gè)雙向消息傳遞系統(tǒng)。

那么,如果只看 迭代器 代碼:

var res = it.next();    // 第一個(gè)`next()`,不傳遞任何東西
res.value;              // "Hello"

res = it.next( 7 );     // 傳遞`7`給等待中的`yield`
res.value;              // 42

注意: 我們沒(méi)有傳遞任何值給第一個(gè)next()調(diào)用,而且是故意的。只有一個(gè)暫停的yield才能接收這樣一個(gè)被next(..)傳遞的值,但是當(dāng)我們調(diào)用第一個(gè)next()時(shí),在generator的最開(kāi)始并 沒(méi)有任何暫停的yield 可以接收這樣的值。語(yǔ)言規(guī)范和所有兼容此語(yǔ)言規(guī)范的瀏覽器只會(huì)無(wú)聲地 丟棄 任何傳入第一個(gè)next()的東西。傳遞這樣的值是一個(gè)壞主意,因?yàn)槟阒徊贿^(guò)創(chuàng)建了一些令人困惑的無(wú)聲“失敗”的代碼。所以,記得總是用一個(gè)無(wú)參數(shù)的next()來(lái)啟動(dòng)generator。

第一個(gè)next()調(diào)用(沒(méi)有任何參數(shù)的)基本上是在 問(wèn)一個(gè)問(wèn)題:“*foo(..)generator將要給我的 下一個(gè) 值是什么?”,誰(shuí)來(lái)回答這個(gè)問(wèn)題?第一個(gè)yield表達(dá)式。

看到了?這里沒(méi)有錯(cuò)位。

根據(jù)你認(rèn)為是 誰(shuí) 在問(wèn)問(wèn)題,在yieldnext(..)之間的錯(cuò)位既存在又不存在。

但等一下!跟yield語(yǔ)句的數(shù)量比起來(lái),還有一個(gè)額外的next()。那么,這個(gè)最后的it.next(7)調(diào)用又一次在詢問(wèn)generator 下一個(gè) 產(chǎn)生的值是什么。但是沒(méi)有yield語(yǔ)句剩下可以回答了,不是嗎?那么誰(shuí)來(lái)回答?

return語(yǔ)句回答這個(gè)問(wèn)題!

而且如果在你的generator中 沒(méi)有return——比起一般的函數(shù),generator中的return當(dāng)然不再是必須的——總會(huì)有一個(gè)假定/隱式的return;(也就是return undefined;),它默認(rèn)的目的就是回答由最后的it.next(7)調(diào)用 提出 的問(wèn)題。

這些問(wèn)題與回答——用yieldnext(..)進(jìn)行雙向消息傳遞——十分強(qiáng)大,但還是看不出來(lái)這些機(jī)制與異步流程控制有什么聯(lián)系。我們正在接近真相!

多迭代器

從語(yǔ)法使用上來(lái)看,當(dāng)你用一個(gè) 迭代器 來(lái)控制generator時(shí),你正在控制聲明的generator函數(shù)本身。但這里有一個(gè)容易忽視的微妙細(xì)節(jié):每當(dāng)你構(gòu)建一個(gè) 迭代器,你都隱含地構(gòu)建了一個(gè)將由這個(gè) 迭代器 控制的generator的實(shí)例。

你可以讓同一個(gè)generator的多個(gè)實(shí)例同時(shí)運(yùn)行,它們甚至可以互動(dòng):

function *foo() {
    var x = yield 2;
    z++;
    var y = yield (x * z);
    console.log( x, y, z );
}

var z = 1;

var it1 = foo();
var it2 = foo();

var val1 = it1.next().value;            // 2 <-- 讓出2
var val2 = it2.next().value;            // 2 <-- 讓出2

val1 = it1.next( val2 * 10 ).value;     // 40  <-- x:20,  z:2
val2 = it2.next( val1 * 5 ).value;      // 600 <-- x:200, z:3

it1.next( val2 / 2 );                   // y:300
                                        // 20 300 3
it2.next( val1 / 4 );                   // y:10
                                        // 200 10 3

警告: 同一個(gè)generator的多個(gè)并發(fā)運(yùn)行實(shí)例的最常見(jiàn)的用法,不是這樣的互動(dòng),而是generator在沒(méi)有輸入的情況下,從一些連接著的獨(dú)立資源中產(chǎn)生它自己的值。我們將在下一節(jié)中更多地討論產(chǎn)生值。

讓我們簡(jiǎn)單地走一遍這個(gè)處理過(guò)程:

  1. 兩個(gè)*foo()在同時(shí)啟動(dòng),而且兩個(gè)next()都分別從yield 2語(yǔ)句中得到了2value。
  2. val2 * 10就是2 * 10,它被發(fā)送到第一個(gè)generator實(shí)例it1,所以x得到值20。z1遞增至2,然后20 * 2yield出來(lái),將val1設(shè)置為40。
  3. val1 * 5就是40 * 5,它被發(fā)送到第二個(gè)generator實(shí)例it2中,所以x得到值200。z又一次遞增,從23,然后200 * 3yield出來(lái),將val2設(shè)置為600。
  4. val2 / 2就是600 / 2,它被發(fā)送到第一個(gè)generator實(shí)例it1,所以y得到值300,然后分別為它的x y z值打印出20 300 3。
  5. val1 / 4就是40 / 4,它被發(fā)送到第一個(gè)generator實(shí)例it2,所以y得到值10,然后分別為它的x y z值打印出200 10 3

這是在你腦海中跑過(guò)的一個(gè)“有趣”的例子。你還能保持清醒?

穿插

回想第一章中“運(yùn)行至完成”一節(jié)的這個(gè)場(chǎng)景:

var a = 1;
var b = 2;

function foo() {
    a++;
    b = b * a;
    a = b + 3;
}

function bar() {
    b--;
    a = 8 + b;
    b = a * 2;
}

使用普通的JS函數(shù),當(dāng)然要么是foo()可以首先運(yùn)行完成,要么是bar()可以首先運(yùn)行至完成,但是foo()不可能與bar()穿插它的獨(dú)立語(yǔ)句。所以,前面這段代碼只有兩個(gè)可能的結(jié)果。

然而,使用generator,明確地穿插(甚至是在語(yǔ)句中間?。┦强赡艿模?/p>

var a = 1;
var b = 2;

function *foo() {
    a++;
    yield;
    b = b * a;
    a = (yield b) + 3;
}

function *bar() {
    b--;
    yield;
    a = (yield 8) + b;
    b = a * (yield 2);
}

根據(jù) 迭代器 控制*foo()*bar()分別以什么樣的順序被調(diào)用,前面這段代碼可以產(chǎn)生幾種不同的結(jié)果。換句話說(shuō),通過(guò)兩個(gè)generator在同一個(gè)共享的變量上穿插,我們實(shí)際上可以展示(以一種模擬的方式)在第一章中討論的,理論上的“線程的競(jìng)合狀態(tài)”環(huán)境。

首先,讓我們制造一個(gè)稱為step(..)的幫助函數(shù),讓它控制 迭代器

function step(gen) {
    var it = gen();
    var last;

    return function() {
        // 不論`yield`出什么,只管在下一次時(shí)直接把它塞回去!
        last = it.next( last ).value;
    };
}

step(..)初始化一個(gè)generator來(lái)創(chuàng)建它的it 迭代器,然后它返回一個(gè)函數(shù),每次這個(gè)函數(shù)被調(diào)用時(shí),都將 迭代器 向前推一步。另外,前一個(gè)被yield出來(lái)的值將被直接發(fā)給下一步。所以,yield 8將變成8yield b將成為b(不管它在yield時(shí)是什么值)。

現(xiàn)在,為了好玩兒,讓我們做一些實(shí)驗(yàn),來(lái)看看將這些*foo()*bar()的不同塊兒穿插時(shí)的效果。我們從一個(gè)無(wú)聊的基本情況開(kāi)始,保證*foo()*bar()之前全部完成(就像我們?cè)诘谝徽轮凶龅哪菢樱?/p>

// 確保重置了`a`和`b`
a = 1;
b = 2;

var s1 = step( foo );
var s2 = step( bar );

// 首先完全運(yùn)行`*foo()`
s1();
s1();
s1();

// 現(xiàn)在運(yùn)行`*bar()`
s2();
s2();
s2();
s2();

console.log( a, b );    // 11 22

最終結(jié)果是1122,就像第一章的版本那樣?,F(xiàn)在讓我們把順序混合穿插,來(lái)看看它如何改變ab的值。

// 確保重置了`a`和`b`
a = 1;
b = 2;

var s1 = step( foo );
var s2 = step( bar );

s2();       // b--;
s2();       // 讓出 8
s1();       // a++;
s2();       // a = 8 + b;
            // 讓出 2
s1();       // b = b * a;
            // 讓出 b
s1();       // a = b + 3;
s2();       // b = a * 2;

在我告訴你結(jié)果之前,你能指出在前面的程序運(yùn)行之后ab的值是什么嗎?不要作弊!

console.log( a, b );    // 12 18

注意: 作為留給讀者的練習(xí),試試通過(guò)重新安排s1()s2()調(diào)用的順序,看看你能得到多少種結(jié)果組合。別忘了你總是需要三個(gè)s1()調(diào)用和四個(gè)s2()調(diào)用。至于為什么,回想一下剛才關(guān)于使用yield匹配next()的討論。

當(dāng)然,你幾乎不會(huì)想有意制造 這種 水平的,令人糊涂的穿插,因?yàn)樗麆?chuàng)建了非常難理解的代碼。但是這個(gè)練習(xí)很有趣,而且對(duì)于理解多個(gè)generator如何并發(fā)地運(yùn)行在相同的共享作用域來(lái)說(shuō)很有教育意義,因?yàn)闀?huì)有一些地方這種能力十分有用。

我們會(huì)在本章末尾更詳細(xì)地討論generator并發(fā)。

生成值

在前一節(jié)中,我們提到了一個(gè)generator的有趣用法,作為一種生產(chǎn)值的方式。這 不是 我們本章主要關(guān)注的,但如果我們不在這里講一下基本我們會(huì)想念它的,特別是因?yàn)檫@種用法實(shí)質(zhì)上是它的名稱的由來(lái):生成器。

我們將要稍稍深入一下 迭代器 的話題,但我們會(huì)繞回到它們?nèi)绾闻cgenerator關(guān)聯(lián),并使用generator來(lái) 生成 值。

發(fā)生器與迭代器

想象你正在生產(chǎn)一系列的值,它們中的每一個(gè)都與前一個(gè)值有可定義的關(guān)系。為此,你將需要一個(gè)有狀態(tài)的發(fā)生器來(lái)記住上一個(gè)給出的值。

你可以用函數(shù)閉包(參加本系列的 作用域與閉包)來(lái)直接地實(shí)現(xiàn)這樣的東西:

var gimmeSomething = (function(){
    var nextVal;

    return function(){
        if (nextVal === undefined) {
            nextVal = 1;
        }
        else {
            nextVal = (3 * nextVal) + 6;
        }

        return nextVal;
    };
})();

gimmeSomething();       // 1
gimmeSomething();       // 9
gimmeSomething();       // 33
gimmeSomething();       // 105

注意: 這里nextVal的計(jì)算邏輯已經(jīng)被簡(jiǎn)化了,但從概念上講,直到 下一次 gimmeSomething()調(diào)用發(fā)生之前,我們不想計(jì)算 下一個(gè)值(也就是nextVal),因?yàn)橐话銓?duì)于持久性更強(qiáng)的,或者比簡(jiǎn)單的number更有限的資源的發(fā)生器來(lái)說(shuō),那可能是一種資源泄漏的設(shè)計(jì)。

生成隨意的數(shù)字序列不是是一個(gè)很真實(shí)的例子。但是如果你從一個(gè)數(shù)據(jù)源中生成記錄呢?你可以想象很多相同的代碼。

事實(shí)上,這種任務(wù)是一種非常常見(jiàn)的設(shè)計(jì)模式,通常用迭代器解決。一個(gè) 迭代器 是一個(gè)明確定義的接口,用來(lái)逐個(gè)通過(guò)一系列從發(fā)生器得到的值。迭代器的JS接口,和大多數(shù)語(yǔ)言一樣,是在你每次想從發(fā)生器中得到下一個(gè)值時(shí)調(diào)用的next()

我們可以為我們的數(shù)字序列發(fā)生器實(shí)現(xiàn)標(biāo)準(zhǔn)的 迭代器;

var something = (function(){
    var nextVal;

    return {
        // `for..of`循環(huán)需要這個(gè)
        [Symbol.iterator]: function(){ return this; },

        // 標(biāo)準(zhǔn)的迭代器接口方法
        next: function(){
            if (nextVal === undefined) {
                nextVal = 1;
            }
            else {
                nextVal = (3 * nextVal) + 6;
            }

            return { done:false, value:nextVal };
        }
    };
})();

something.next().value;     // 1
something.next().value;     // 9
something.next().value;     // 33
something.next().value;     // 105

注意: 我們將在“Iterables”一節(jié)中講解為什么我們?cè)谶@個(gè)代碼段中需要[Symbol.iterator]: ..這一部分。在語(yǔ)法上講,兩個(gè)ES6特性在發(fā)揮作用。首先,[ .. ]語(yǔ)法稱為一個(gè) 計(jì)算型屬性名(參見(jiàn)本系列的 this與對(duì)象原型)。它是一種字面對(duì)象定義方法,用來(lái)指定一個(gè)表達(dá)式并使用這個(gè)表達(dá)式的結(jié)果作為屬性名。另一個(gè),Symbol.iterator是ES6預(yù)定義的特殊Symbol值。

next()調(diào)用返回一個(gè)對(duì)象,它帶有兩個(gè)屬性:done是一個(gè)boolean值表示 迭代器 的完成狀態(tài);value持有迭代的值。

ES6還增加了for..of循環(huán),它意味著一個(gè)標(biāo)準(zhǔn)的 迭代器 可以使用原生的循環(huán)語(yǔ)法來(lái)自動(dòng)地被消費(fèi):

for (var v of something) {
    console.log( v );

    // 不要讓循環(huán)永無(wú)休止!
    if (v > 500) {
        break;
    }
}
// 1 9 33 105 321 969

注意: 因?yàn)槲覀兊?code>something迭代器總是返回done:false,這個(gè)for..of循環(huán)將會(huì)永遠(yuǎn)運(yùn)行,這就是為什么我們條件性地放進(jìn)一個(gè)break。對(duì)于迭代器來(lái)說(shuō)永不終結(jié)是完全沒(méi)有問(wèn)題的,但是也有一些情況 迭代器 將運(yùn)行在有限的值的集合上,而最終返回done:true。

for..of循環(huán)為每一次迭代自動(dòng)調(diào)用next()——他不會(huì)給next()傳入任何值——而且他將會(huì)在收到一個(gè)done:true時(shí)自動(dòng)終結(jié)。這對(duì)于在一個(gè)集合的數(shù)據(jù)中進(jìn)行循環(huán)十分方便。

當(dāng)然,你可以手動(dòng)循環(huán)一個(gè)迭代器,調(diào)用next()并檢查done:true條件來(lái)知道什么時(shí)候停止:

for (
    var ret;
    (ret = something.next()) && !ret.done;
) {
    console.log( ret.value );

    // 不要讓循環(huán)永無(wú)休止!
    if (ret.value > 500) {
        break;
    }
}
// 1 9 33 105 321 969

注意: 這種手動(dòng)的for方式當(dāng)然要比ES6的for..of循環(huán)語(yǔ)法難看,但它的好處是它提供給你一個(gè)機(jī)會(huì),在有必要時(shí)傳值給next(..)調(diào)用。

除了制造你自己的 迭代器 之外,許多JS中(就ES6來(lái)說(shuō))內(nèi)建的數(shù)據(jù)結(jié)構(gòu),比如array,也有默認(rèn)的 迭代器

var a = [1,3,5,7,9];

for (var v of a) {
    console.log( v );
}
// 1 3 5 7 9

for..of循環(huán)向a要來(lái)它的迭代器,并自動(dòng)使用它迭代a的值。

注意: 看起來(lái)像是一個(gè)ES6的奇怪省略,普通的object有意地不帶有像array那樣的默認(rèn) 迭代器。原因比我們要在這里講的深刻得多。如果你想要的只是迭代一個(gè)對(duì)象的屬性(不特別保證順序),Object.keys(..)返回一個(gè)array,它可以像for (var k of Object.keys(obj)) { ..這樣使用。像這樣用for..of循環(huán)一個(gè)對(duì)象上的鍵,與用for..in循環(huán)內(nèi)很相似,除了在for..in中會(huì)包含[[Prototype]]鏈的屬性,而Object.keys(..)不會(huì)(參見(jiàn)本系列的 this與對(duì)象原型)。

Iterables

在我們運(yùn)行的例子中的something對(duì)象被稱為一個(gè) 迭代器,因?yàn)樗慕涌谥杏?code>next()方法。但一個(gè)緊密關(guān)聯(lián)的術(shù)語(yǔ)是 iterable,它指 包含有 一個(gè)可以迭代它所有值的迭代器的對(duì)象。

在ES6中,從一個(gè) iterable 中取得一個(gè) 迭代器 的方法是,iterable 上必須有一個(gè)函數(shù),它的名稱是特殊的ES6符號(hào)值Symbol.iterator。當(dāng)這個(gè)函數(shù)被調(diào)用時(shí),它就會(huì)返回一個(gè) 迭代器。雖然不是必須的,但一般來(lái)說(shuō)每次調(diào)用應(yīng)當(dāng)返回一個(gè)全新的 迭代器。

前一個(gè)代碼段的a就是一個(gè) iterablefor..of循環(huán)自動(dòng)地調(diào)用它的Symbol.iterator函數(shù)來(lái)構(gòu)建一個(gè) 迭代器。我們當(dāng)然可以手動(dòng)地調(diào)用這個(gè)函數(shù),然后使用它返回的 iterator

var a = [1,3,5,7,9];

var it = a[Symbol.iterator]();

it.next().value;    // 1
it.next().value;    // 3
it.next().value;    // 5
..

在前面定義something的代碼段中,你可能已經(jīng)注意到了這一行:

[Symbol.iterator]: function(){ return this; }

這段有點(diǎn)讓人困惑的代碼制造了something值——something迭代器 的接口——也是一個(gè) iterable;現(xiàn)在它既是一個(gè) iterable 也是一個(gè) 迭代器。然后,我們把something傳遞給for..of循環(huán):

for (var v of something) {
    ..
}

for..of循環(huán)期待something是一個(gè) iterable,所以它會(huì)尋找并調(diào)用它的Symbol.iterator函數(shù)。我們將這個(gè)函數(shù)定義為簡(jiǎn)單地return this,所以它將自己給出,而for..of不會(huì)知道這些。

Generator迭代器

帶著 迭代器 的背景知識(shí),讓我們把注意力移回generator。一個(gè)generator可以被看做一個(gè)值的發(fā)生器,我們通過(guò)一個(gè) 迭代器 接口的next()調(diào)用每次從中抽取一個(gè)值。

所以,一個(gè)generator本身在技術(shù)上講并不是一個(gè) iterable,雖然很相似——當(dāng)你執(zhí)行g(shù)enerator時(shí),你就得到一個(gè) 迭代器

function *foo(){ .. }

var it = foo();

我們可以用generator實(shí)現(xiàn)早前的something無(wú)限數(shù)字序列發(fā)生器,就像這樣:

function *something() {
    var nextVal;

    while (true) {
        if (nextVal === undefined) {
            nextVal = 1;
        }
        else {
            nextVal = (3 * nextVal) + 6;
        }

        yield nextVal;
    }
}

注意: 在一個(gè)真實(shí)的JS程序中含有一個(gè)while..true循環(huán)通常是一件非常不好的事情,至少如果它沒(méi)有一個(gè)breakreturn語(yǔ)句,那么它就很可能永遠(yuǎn)運(yùn)行,并同步地,阻塞/鎖定瀏覽器UI。然而,在generator中,如果這樣的循環(huán)含有一個(gè)yield,那它就是完全沒(méi)有問(wèn)題的,因?yàn)間enerator將在每次迭代后暫停,yield回主程序和/或事件輪詢隊(duì)列。說(shuō)的明白點(diǎn)兒,“generator把while..true帶回到JS編程中了!”

這變得相當(dāng)干凈和簡(jiǎn)單點(diǎn)兒了,對(duì)吧?因?yàn)間enerator會(huì)暫停在每個(gè)yield,*something()函數(shù)的狀態(tài)(作用域)被保持著,這意味著沒(méi)有必要用閉包的模板代碼來(lái)跨調(diào)用保留變量的狀態(tài)了。

不僅是更簡(jiǎn)單的代碼——我們不必自己制造 迭代器 接口了——它實(shí)際上是更合理的代碼,因?yàn)樗逦乇磉_(dá)了意圖。比如,while..true循環(huán)告訴我們這個(gè)generator將要永遠(yuǎn)運(yùn)行——只要我們一直向它請(qǐng)求,它就一直 產(chǎn)生 值。

現(xiàn)在我們可以在for..of循環(huán)中使用新得發(fā)亮的*something()generator了,而且你會(huì)看到它工作起來(lái)基本一模一樣:

for (var v of something()) {
    console.log( v );

    // 不要讓循環(huán)永無(wú)休止!
    if (v > 500) {
        break;
    }
}
// 1 9 33 105 321 969

不要跳過(guò)for (var v of something()) ..!我們不僅僅像之前的例子那樣將something作為一個(gè)值引用了,而是調(diào)用*something()generator來(lái)得到它的 迭代器,并交給for..of使用。

如果你仔細(xì)觀察,在這個(gè)generator和循環(huán)的互動(dòng)中,你可能會(huì)有兩個(gè)疑問(wèn):

  • 為什么我們不能說(shuō)for (var v of something) ..?因?yàn)檫@個(gè)something是一個(gè)generator,而不是一個(gè) iterable。我們不得不調(diào)用something()來(lái)構(gòu)建一個(gè)發(fā)生器給for..of,以便它可以迭代。
  • something()調(diào)用創(chuàng)建一個(gè) 迭代器,但是for..of想要一個(gè) iterable,對(duì)吧?對(duì),generator的 迭代器 上也有一個(gè)Symbol.iterator函數(shù),這個(gè)函數(shù)基本上就是return this,就像我們剛才定義的somethingiterable。換句話說(shuō)generator的 迭代器 也是一個(gè) iterable!

停止Generator

在前一個(gè)例子中,看起來(lái)在循環(huán)的break被調(diào)用后,*something()generator的 迭代器 實(shí)例基本上被留在了一個(gè)永遠(yuǎn)掛起的狀態(tài)。

但是這里有一個(gè)隱藏的行為為你處理這件事。for..of循環(huán)的“異常完成”(“提前終結(jié)”等等)——一般是由break,return,或未捕捉的異常導(dǎo)致的——會(huì)向generator的 迭代器 發(fā)送一個(gè)信號(hào),以使它終結(jié)。

注意: 技術(shù)上講,for..of循環(huán)也會(huì)在循環(huán)正常完成時(shí)向 迭代器 發(fā)送這個(gè)信號(hào)。對(duì)于generator來(lái)說(shuō),這實(shí)質(zhì)上是一個(gè)無(wú)實(shí)際意義的操作,因?yàn)間enerator的 迭代器 要首先完成,for..of循環(huán)才能完成。然而,自定義的 迭代器 可能會(huì)希望從for..of循環(huán)的消費(fèi)者那里得到另外的信號(hào)。

雖然一個(gè)for..of循環(huán)將會(huì)自動(dòng)發(fā)送這種信號(hào),你可能會(huì)希望手動(dòng)發(fā)送信號(hào)給一個(gè) 迭代器;你可以通過(guò)調(diào)用return(..)來(lái)這么做。

如果你在generator內(nèi)部指定一個(gè)try..finally從句,它將總是被執(zhí)行,即便是generator從外部被完成。這在你需要進(jìn)行資源清理時(shí)很有用(數(shù)據(jù)庫(kù)連接等):

function *something() {
    try {
        var nextVal;

        while (true) {
            if (nextVal === undefined) {
                nextVal = 1;
            }
            else {
                nextVal = (3 * nextVal) + 6;
            }

            yield nextVal;
        }
    }
    // 清理用的從句
    finally {
        console.log( "cleaning up!" );
    }
}

前面那個(gè)在for..of中帶有break的例子將會(huì)觸發(fā)finally從句。但是你可以用return(..)從外部來(lái)手動(dòng)終結(jié)generator的 迭代器 實(shí)例:

var it = something();
for (var v of it) {
    console.log( v );

    // 不要讓循環(huán)永無(wú)休止!
    if (v > 500) {
        console.log(
            // 使generator得迭代器完成
            it.return( "Hello World" ).value
        );
        // 這里不需要`break`
    }
}
// 1 9 33 105 321 969
// cleaning up!
// Hello World

當(dāng)我們調(diào)用it.return(..)時(shí),它會(huì)立即終結(jié)generator,從而運(yùn)行finally從句。而且,它會(huì)將返回的value設(shè)置為你傳入return(..)的任何東西,這就是Hellow World如何立即返回來(lái)的。我們現(xiàn)在也不必再包含一個(gè)break,因?yàn)間enerator的 迭代器 會(huì)被設(shè)置為done:true,所以for..of循環(huán)會(huì)在下一次迭代時(shí)終結(jié)。

generator的命名大部分源自于這種 消費(fèi)生產(chǎn)的值 的用法。但要重申的是,這只是generator的用法之一,而且坦白的說(shuō),在這本書(shū)的背景下這甚至不是我們主要關(guān)注的。

但是現(xiàn)在我們更加全面地了解它們的機(jī)制是如何工作的,我們接下來(lái)可以將注意力轉(zhuǎn)向generator如何實(shí)施于異步并發(fā)。

異步地迭代Generator

generator要怎樣處理異步編碼模式,解決回調(diào)和類似的問(wèn)題?讓我們開(kāi)始回答這個(gè)重要的問(wèn)題。

我們應(yīng)當(dāng)重溫一下第三章的一個(gè)場(chǎng)景?;叵胍幌逻@個(gè)回調(diào)方式:

function foo(x,y,cb) {
    ajax(
        "http://some.url.1/?x=" + x + "&y=" + y,
        cb
    );
}

foo( 11, 31, function(err,text) {
    if (err) {
        console.error( err );
    }
    else {
        console.log( text );
    }
} );

如果我們想用generator表示相同的任務(wù)流控制,我們可以:

function foo(x,y) {
    ajax(
        "http://some.url.1/?x=" + x + "&y=" + y,
        function(err,data){
            if (err) {
                // 向`*main()`中扔進(jìn)一個(gè)錯(cuò)誤
                it.throw( err );
            }
            else {
                // 使用收到的`data`來(lái)繼續(xù)`*main()`
                it.next( data );
            }
        }
    );
}

function *main() {
    try {
        var text = yield foo( 11, 31 );
        console.log( text );
    }
    catch (err) {
        console.error( err );
    }
}

var it = main();

// 使一切開(kāi)始運(yùn)行!
it.next();

一眼看上去,這個(gè)代碼段要比以前的回調(diào)代碼更長(zhǎng),而且也許看起來(lái)更復(fù)雜。但不要讓這種印象誤導(dǎo)你。generator的代碼段實(shí)際上要好 太多 了!但是這里有很多我們需要講解的。

首先,讓我們看看代碼的這一部分,也是最重要的部分:

var text = yield foo( 11, 31 );
console.log( text );

花一點(diǎn)時(shí)間考慮一下這段代碼如何工作。我們調(diào)用了一個(gè)普通的函數(shù)foo(..),而且我們顯然可以從Ajax調(diào)用那里得到text,即便它是異步的。

這怎么可能?如果你回憶一下第一章的最開(kāi)始,我們有一個(gè)幾乎完全一樣的代碼:

var data = ajax( "..url 1.." );
console.log( data );

但是這段代碼不好用!你能發(fā)現(xiàn)不同嗎?它就是在generator中使用的yield。

這就是魔法發(fā)生的地方!是它允許我們擁有一個(gè)看起來(lái)是阻塞的,同步的,但實(shí)際上不會(huì)阻塞整個(gè)程序的代碼;它僅僅暫停/阻塞在generator本身的代碼。

yield foo(11,31)中,首先foo(11,31)調(diào)用被發(fā)起,它什么也不返回(也就是undefined),所以我們發(fā)起了數(shù)據(jù)請(qǐng)求,然后我們實(shí)際上做的是yield undefined。這沒(méi)問(wèn)題,因?yàn)檫@段代碼現(xiàn)在沒(méi)有依賴yield的值來(lái)做任何有趣的事。我們?cè)诒菊律院笤僦匦掠懻撨@個(gè)問(wèn)題。

在這里,我們沒(méi)有將yield作為消息傳遞的工具,只是作為進(jìn)行暫停/阻塞的流程控制的工具。實(shí)際上,它會(huì)傳遞消息,但是只是單向的,在generator被繼續(xù)運(yùn)行之后。

那么,generator暫停在了yield,它實(shí)質(zhì)上再問(wèn)一個(gè)問(wèn)題,“我該將什么值返回并賦給變量text?”誰(shuí)來(lái)回答這個(gè)問(wèn)題?

看一下foo(..)。如果Ajax請(qǐng)求成功,我們調(diào)用:

it.next( data );

這將使generator使用應(yīng)答數(shù)據(jù)繼續(xù)運(yùn)行,這意味著我們暫停的yield表達(dá)式直接收到這個(gè)值,然后因?yàn)樗匦麻_(kāi)始以運(yùn)行g(shù)enerator代碼,所以這個(gè)值被賦給本地變量text

很酷吧?

退一步考慮一下它的意義。我們?cè)趃enerator內(nèi)部的代碼看起來(lái)完全是同步的(除了yield關(guān)鍵字本身),但隱藏在幕后的是,在foo(..)內(nèi)部,操作可以完全是異步的。

這很偉大! 這幾乎完美地解決了我們前面遇到的問(wèn)題:回調(diào)不能像我們的大腦可以關(guān)聯(lián)的那樣,以一種順序,同步的風(fēng)格表達(dá)異步處理。

實(shí)質(zhì)上,我們將異步處理作為實(shí)現(xiàn)細(xì)節(jié)抽象出去,以至于我們可以同步地/順序地推理我們的流程控制:“發(fā)起Ajax請(qǐng)求,然后在它完成之后打印應(yīng)答?!?當(dāng)然,我們僅僅在這個(gè)流程控制中表達(dá)了兩個(gè)步驟,但同樣的能力可以無(wú)邊界地延伸,讓我們需要表達(dá)多少步驟,就表達(dá)多少。

提示: 這是一個(gè)如此重要的認(rèn)識(shí),為了充分理解,現(xiàn)在回過(guò)頭去再把最后三段讀一遍!

同步錯(cuò)誤處理

但是前面的generator代碼會(huì) 出更多的好處給我們。讓我們把注意力移到generator內(nèi)部的try..catch上:

try {
    var text = yield foo( 11, 31 );
    console.log( text );
}
catch (err) {
    console.error( err );
}

這是怎么工作的?foo(..)調(diào)用是異步完成的,try..catch不是無(wú)法捕捉異步錯(cuò)誤嗎?就像我們?cè)诘谌轮锌吹降模?/p>

我們已經(jīng)看到了yield如何讓賦值語(yǔ)句暫停,來(lái)等待foo(..)去完成,以至于完成的響應(yīng)可以被賦予text。牛X的是,yield暫停 允許generator來(lái)catch一個(gè)錯(cuò)誤。我們?cè)谇懊娴睦?,我們用這一部分代碼將這個(gè)錯(cuò)誤拋出到generator中:

if (err) {
    // 向`*main()`中扔進(jìn)一個(gè)錯(cuò)誤
    it.throw( err );
}

generator的yield暫停特性不僅意味著我們可以從異步的函數(shù)調(diào)用那里得到看起來(lái)同步的return值,還意味著我們可以同步地捕獲這些異步函數(shù)調(diào)用的錯(cuò)誤!

那么我們看到了,我們可以將錯(cuò)誤 拋入 generator,但是將錯(cuò)誤 拋出 一個(gè)generator呢?和你期望的一樣:

function *main() {
    var x = yield "Hello World";

    yield x.toLowerCase();  // 引發(fā)一個(gè)異常!
}

var it = main();

it.next().value;            // Hello World

try {
    it.next( 42 );
}
catch (err) {
    console.error( err );   // TypeError
}

當(dāng)然,我們本可以用throw ..手動(dòng)地拋出一個(gè)錯(cuò)誤,而不是制造一個(gè)異常。

我們甚至可以catch我們throw(..)進(jìn)generator的同一個(gè)錯(cuò)誤,實(shí)質(zhì)上給了generator一個(gè)機(jī)會(huì)來(lái)處理它,但如果generator沒(méi)處理,那么 迭代器 代碼必須處理它:

function *main() {
    var x = yield "Hello World";

    // 永遠(yuǎn)不會(huì)跑到這里
    console.log( x );
}

var it = main();

it.next();

try {
    // `*main()`會(huì)處理這個(gè)錯(cuò)誤嗎?我們走著瞧!
    it.throw( "Oops" );
}
catch (err) {
    // 不,它沒(méi)處理!
    console.error( err );           // Oops
}

使用異步代碼的,看似同步的錯(cuò)誤處理(通過(guò)try..catch)在可讀性和可推理性上大獲全勝。

Generators + Promises

在我們前面的討論中,我們展示了generator如何可以異步地迭代,這是一個(gè)用順序的可推理性來(lái)取代混亂如面條的回調(diào)的一個(gè)巨大進(jìn)步。但我們丟掉了兩個(gè)非常重要的東西:Promise的可靠性和可組合性(見(jiàn)第三章)!

別擔(dān)心——我們會(huì)把它們拿回來(lái)。在ES6的世界中最棒的就是將generator(看似同步的異步代碼)與Promise(可靠性和可組合性)組合起來(lái)。

但怎么做呢?

回想一下第三章中我們基于Promise的方式運(yùn)行Ajax的例子:

function foo(x,y) {
    return request(
        "http://some.url.1/?x=" + x + "&y=" + y
    );
}

foo( 11, 31 )
.then(
    function(text){
        console.log( text );
    },
    function(err){
        console.error( err );
    }
);

在我們?cè)缦鹊倪\(yùn)行Ajax的例子的generator代碼中,foo(..)什么也不返回(undefined),而且我們的 迭代器 控制代碼也不關(guān)心yield的值。

但這里的Promise相關(guān)的foo(..)在發(fā)起Ajax調(diào)用后返回一個(gè)promise。這暗示著我們可以用foo(..)構(gòu)建一個(gè)promise,然后從generator中yield出來(lái),而后 迭代器 控制代碼將可以收到這個(gè)promise。

那么 迭代器 應(yīng)當(dāng)對(duì)promise做什么?

它應(yīng)當(dāng)監(jiān)聽(tīng)promise的解析(完成或拒絕),然后要么使用完成消息繼續(xù)運(yùn)行g(shù)enerator,要么使用拒絕理由向generator拋出錯(cuò)誤。

讓我重復(fù)一遍,因?yàn)樗绱酥匾?。發(fā)揮Promise和generator的最大功效的自然方法是 yield一個(gè)Promise,并將這個(gè)Promise連接到generator的 迭代器 的控制端。

讓我們?cè)囈幌拢∈紫?,我們將Promise相關(guān)的foo(..)與generator*main()放在一起:

function foo(x,y) {
    return request(
        "http://some.url.1/?x=" + x + "&y=" + y
    );
}

function *main() {
    try {
        var text = yield foo( 11, 31 );
        console.log( text );
    }
    catch (err) {
        console.error( err );
    }
}

在這個(gè)重構(gòu)中最強(qiáng)大的啟示是,*main()內(nèi)部的代碼 更本就沒(méi)變! 在generator內(nèi)部,無(wú)論什么樣的值被yield出去都是一個(gè)不可見(jiàn)的實(shí)現(xiàn)細(xì)節(jié),所以我們甚至不會(huì)察覺(jué)它發(fā)生了,也不用擔(dān)心它。

那么我們現(xiàn)在如何運(yùn)行*main()?我們還有一些管道的實(shí)現(xiàn)工作要做,接收并連接yield的promise,使它能夠根據(jù)解析來(lái)繼續(xù)運(yùn)行g(shù)enerator。我們從手動(dòng)這么做開(kāi)始:

var it = main();

var p = it.next().value;

// 等待`p` promise解析
p.then(
    function(text){
        it.next( text );
    },
    function(err){
        it.throw( err );
    }
);

其實(shí),根本不費(fèi)事,對(duì)吧?

這段代碼應(yīng)當(dāng)看起來(lái)與我們?cè)缜白龅暮芟嗨疲菏謩?dòng)地連接被錯(cuò)誤優(yōu)先的回調(diào)控制的generator。與if (err) { it.throw..不同的是,promise已經(jīng)為我們分割為完成(成功)與拒絕(失?。?,否則 迭代器 控制是完全相同的。

現(xiàn)在,我們已經(jīng)掩蓋了一些重要的細(xì)節(jié)。

最重要的是,我們利用了這樣一個(gè)事實(shí):我們知道*main()里面只有一個(gè)Promise相關(guān)的步驟。如果我們想要能用Promise驅(qū)動(dòng)一個(gè)generator而不管它有多少步驟呢?我們當(dāng)然不想為每一個(gè)generator手動(dòng)編寫一個(gè)不同的Promise鏈!要是有這樣一種方法該多好:可以重復(fù)(也就是“循環(huán)”)迭代的控制,而且每次一有Promise出來(lái),就在繼續(xù)之前等待它的解析。

另外,如果generator在it.next()調(diào)用期間拋出一個(gè)錯(cuò)誤怎么辦?我們是該退出,還是應(yīng)該catch它并把它送回去?相似地,要是我們it.throw(..)一個(gè)Promise拒絕給generator,但是沒(méi)有被處理,又直接回來(lái)了呢?

帶有Promise的Generator運(yùn)行器

你在這條路上探索得越遠(yuǎn),你就越能感到,“哇,要是有一些工具能幫我做這些就好了?!倍夷憬^對(duì)是對(duì)的。這是一種如此重要的模式,而且你不想把它弄錯(cuò)(或者因?yàn)橐槐橛忠槐榈刂貜?fù)它而把自己累死),所以你最好的選擇是把賭注壓在一個(gè)工具上,而它以我們將要描述的方式使用這種特定設(shè)計(jì)的工具來(lái) 運(yùn)行 yieldPromise的generator。

有幾種Promise抽象庫(kù)提供了這樣的工具,包括我的 asynquence 庫(kù)和它的runner(..),我們將在本書(shū)的在附錄A中討論它。

但看在學(xué)習(xí)和講解的份兒上,讓我們定義我們自己的名為run(..)的獨(dú)立工具:

// 感謝Benjamin Gruenbaum (@benjamingr在GitHub)在此做出的巨大改進(jìn)!
function run(gen) {
    var args = [].slice.call( arguments, 1), it;

    // 在當(dāng)前的上下文環(huán)境中初始化generator
    it = gen.apply( this, args );

    // 為generator的完成返回一個(gè)promise
    return Promise.resolve()
        .then( function handleNext(value){
            // 運(yùn)行至下一個(gè)讓出的值
            var next = it.next( value );

            return (function handleResult(next){
                // generator已經(jīng)完成運(yùn)行了?
                if (next.done) {
                    return next.value;
                }
                // 否則繼續(xù)執(zhí)行
                else {
                    return Promise.resolve( next.value )
                        .then(
                            // 在成功的情況下繼續(xù)異步循環(huán),將解析的值送回generator
                            handleNext,

                            // 如果`value`是一個(gè)拒絕的promise,就將錯(cuò)誤傳播回generator自己的錯(cuò)誤處理g
                            function handleErr(err) {
                                return Promise.resolve(
                                    it.throw( err )
                                )
                                .then( handleResult );
                            }
                        );
                }
            })(next);
        } );
}

如你所見(jiàn),它可能比你想要自己編寫的東西復(fù)雜得多,特別是你將不會(huì)想為每個(gè)你使用的generator重復(fù)這段代碼。所以,一個(gè)幫助工具/庫(kù)絕對(duì)是可行的。雖然,我鼓勵(lì)你花幾分鐘時(shí)間研究一下這點(diǎn)代碼,以便對(duì)如何管理generator+Promise交涉得到更好的感覺(jué)。

你如何在我們 正在討論 的Ajax例子中將run(..)*main()一起使用呢?

function *main() {
    // ..
}

run( main );

就是這樣!按照我們連接run(..)的方式,它將自動(dòng)地,異步地推進(jìn)你傳入的generator,直到完成。

注意: 我們定義的run(..)返回一個(gè)promise,它被連接成一旦generator完成就立即解析,或者收到一個(gè)未捕獲的異常,而generator沒(méi)有處理它。我們沒(méi)有在這里展示這種能力,但我們會(huì)在本章稍后回到這個(gè)話題。

ES7: asyncawait

前面的模式——generator讓出一個(gè)Promise,然后這個(gè)Promise控制generator的 迭代器 向前推進(jìn)至它完成——是一個(gè)如此強(qiáng)大和有用的方法,如果我們能不通過(guò)亂七八糟的幫助工具庫(kù)(也就是run(..))來(lái)使用它就更好了。

在這方面可能有一些好消息。在寫作這本書(shū)的時(shí)候,后ES6,ES7化的時(shí)間表上已經(jīng)出現(xiàn)了草案,對(duì)這個(gè)問(wèn)題提供早期但強(qiáng)大的附加語(yǔ)法支持。顯然,現(xiàn)在還太早而不能保證其細(xì)節(jié),但是有相當(dāng)大的可能性它將蛻變?yōu)轭愃朴谙旅娴臇|西:

function foo(x,y) {
    return request(
        "http://some.url.1/?x=" + x + "&y=" + y
    );
}

async function main() {
    try {
        var text = await foo( 11, 31 );
        console.log( text );
    }
    catch (err) {
        console.error( err );
    }
}

main();

如你所見(jiàn),這里沒(méi)有run(..)調(diào)用(意味著不需要工具庫(kù)!)來(lái)驅(qū)動(dòng)和調(diào)用main()——它僅僅像一個(gè)普通函數(shù)那樣被調(diào)用。另外,main()不再作為一個(gè)generator函數(shù)聲明;它是一種新型的函數(shù):async function。而最后,與yield一個(gè)Promise相反,我們await它解析。

如果你await一個(gè)Promise,async function會(huì)自動(dòng)地知道做什么——它會(huì)暫停這個(gè)函數(shù)(就像使用generator那樣)直到Promise解析。我們沒(méi)有在這個(gè)代碼段中展示,但是調(diào)用一個(gè)像main()這樣的異步函數(shù)將自動(dòng)地返回一個(gè)promise,它會(huì)在函數(shù)完全完成時(shí)被解析。

提示: async / await的語(yǔ)法應(yīng)該對(duì)擁有C#經(jīng)驗(yàn)的讀者看起來(lái)非常熟悉,因?yàn)樗鼈兓旧鲜且粯拥摹?/p>

這個(gè)草案實(shí)質(zhì)上是為我們已經(jīng)衍生出的模式進(jìn)行代碼化的支持,成為一種語(yǔ)法機(jī)制:用看似同步的流程控制代碼與Promise組合。將兩個(gè)世界的最好部分組合,來(lái)有效解決我們用回調(diào)遇到的幾乎所有主要問(wèn)題。

這樣的ES7化草案已經(jīng)存在,并且有了早期的支持和熱忱的擁護(hù)。這一事實(shí)為這種異步模式在未來(lái)的重要性上信心滿滿地投了有力的一票。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容