你不知道JS:異步(翻譯)系列4-2

你不知道JS:異步

第四章:生成器(Generators)

接上篇4-1

生成器委托(Generator Delegation)

在前一節(jié)中,我們展示了從生成器內(nèi)部調(diào)用普通函數(shù),以及為什么抽離實(shí)現(xiàn)細(xì)節(jié)是個(gè)有用的技術(shù)(像異步Promise流)。但采用普通函數(shù)的主要缺點(diǎn)是必須遵循不同函數(shù)規(guī)則,這意味著無法像生成器一樣使用yield來暫停函數(shù)自身。

你突然想到,通過輔助函數(shù)run(..),可以試著從另一個(gè)生成器中調(diào)用生成器,形如:

function *foo() {
    var r2 = yield request( "http://some.url.2" );
    var r3 = yield request( "http://some.url.3/?v=" + r2 );

    return r3;
}

function *bar() {
    var r1 = yield request( "http://some.url.1" );

    // "delegating" to `*foo()` via `run(..)`
    var r3 = yield run( foo );

    console.log( r3 );
}

run( bar );

通過使用run(..) utility,我們在*bar()內(nèi)部運(yùn)行*foo()。此處,我們利用了這一事實(shí),即早先定義的run(..)返回一個(gè)promise,當(dāng)該生成器運(yùn)行直至結(jié)束(或者發(fā)生錯(cuò)誤)時(shí),該promise得到解析。因此,如果我們yield出另一個(gè)run(..)調(diào)用生成的promise給run(..)實(shí)例,它會自動暫停*bar()直至*foo()完成。

但是有個(gè)更好的方法來整合*bar()內(nèi)的*foo()調(diào)用,稱為yield委托。yield委托的特殊語法是:yield * _(注意多出的*)。在我們看它如何在之前例子中工作之前,先看一個(gè)簡單點(diǎn)的場景:

function *foo() {
    console.log( "`*foo()` starting" );
    yield 3;
    yield 4;
    console.log( "`*foo()` finished" );
}

function *bar() {
    yield 1;
    yield 2;
    yield *foo();   // `yield`-delegation!
    yield 5;
}

var it = bar();

it.next().value;    // 1
it.next().value;    // 2
it.next().value;    // `*foo()` starting
                    // 3
it.next().value;    // 4
it.next().value;    // `*foo()` finished
                    // 5

注意: 類似于本章早些時(shí)候的注意事項(xiàng),即為什么我偏愛function *foo() ..而不是function* foo() ..,同樣,我也偏愛--不同于其它大多數(shù)相關(guān)文檔--用yield *foo(),而不是yield* foo()。*的位置純粹是風(fēng)格上的喜好,由你自己決定。但我覺得風(fēng)格一致比較有吸引力。

yield *foo()委托是如何工作的呢?

首先,foo()調(diào)用創(chuàng)建了一個(gè)迭代器。之后,yield *委托/轉(zhuǎn)移迭代器實(shí)例的控制權(quán)(當(dāng)前*bar()生成器的)給另一個(gè)*foo()迭代器。

因此,前兩個(gè)it.next()調(diào)用控制*bar(),但是當(dāng)進(jìn)行第三個(gè)it.next()調(diào)用時(shí),*foo()啟動了,現(xiàn)在我們控制foo()而不是*bar()了。這就是稱為委托的原因--*bar()將自己迭代的控制權(quán)委托給*foo()。

一旦it迭代器控制迭代完整個(gè)*foo()迭代器,就會自動返回到*bar()控制之中。

現(xiàn)在回到之前的三個(gè)序列化Ajax請求的例子:

function *foo() {
    var r2 = yield request( "http://some.url.2" );
    var r3 = yield request( "http://some.url.3/?v=" + r2 );

    return r3;
}

function *bar() {
    var r1 = yield request( "http://some.url.1" );

    // "delegating" to `*foo()` via `yield*`
    var r3 = yield *foo();

    console.log( r3 );
}

run( bar );

和早先版本唯一的不同是采用了yield *foo(),而不是之前的yield run(foo)

注意: yield * yield出迭代控制權(quán),而不是生成器的控制權(quán);當(dāng)你激活*foo()生成器時(shí),yield委托到它的迭代器。但實(shí)際上也可以yield委托任何iterable,yield *[1,2,3]會處理[1,2,3]的默認(rèn)迭代器

為什么委托?(Why Delegation?)

yield委托的主要目的是組織代碼,那樣的話就和普通的函數(shù)調(diào)用沒什么區(qū)別了。

假設(shè)兩個(gè)模塊分別提供了foo()bar()方法,bar()調(diào)用foo()。
分開的原因通常是為合理的代碼組織考慮,即可能在不同的函數(shù)中調(diào)用它們。比如,可能有些時(shí)候,foo()是單獨(dú)調(diào)用的,有時(shí)是bar()調(diào)用foo()。

幾乎基于同樣的原因,即保持生成器分離有助于提高程序的可讀性、可維護(hù)性和可調(diào)試性。從那個(gè)角度講,當(dāng)在*bar()內(nèi)部時(shí),yield *是手動迭代*foo()步驟的簡寫形式。

如果*foo()的步驟是異步的,手動方法可能特別復(fù)雜,這就是為什么需要run(..)utility。如上所示,yield *foo()就不需要run(..)utility的子實(shí)例(比如run(foo))了。

委托信息(Delegating Messages)

你可能想知道yield委托是如何實(shí)現(xiàn)迭代器控制和兩路信息傳遞的。通過yield委托,仔細(xì)觀察信息的流入、流出:

function *foo() {
    console.log( "inside `*foo()`:", yield "B" );

    console.log( "inside `*foo()`:", yield "C" );

    return "D";
}

function *bar() {
    console.log( "inside `*bar()`:", yield "A" );

    // `yield`-delegation!
    console.log( "inside `*bar()`:", yield *foo() );

    console.log( "inside `*bar()`:", yield "E" );

    return "F";
}

var it = bar();

console.log( "outside:", it.next().value );
// outside: A

console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B

console.log( "outside:", it.next( 2 ).value );
// inside `*foo()`: 2
// outside: C

console.log( "outside:", it.next( 3 ).value );
// inside `*foo()`: 3
// inside `*bar()`: D
// outside: E

console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: 4
// outside: F

特別關(guān)注一下it.next(3)調(diào)用后的處理步驟:

  1. 3被傳入*foo()內(nèi)(通過*bar()內(nèi)的yield委托)等待的yield "C"表達(dá)式。
  2. 之后*foo()調(diào)用return "D",但這個(gè)值并沒有返回給外面的it.next(3)
  3. 反而,D值返回作為*bar()內(nèi)等待的yield *foo()表達(dá)式的結(jié)果--當(dāng)*foo()被窮盡時(shí),這種yield委托表達(dá)本質(zhì)上已經(jīng)被暫停了。因此*bar()內(nèi)的"D"最終被打印出來了。
  4. yield "E"*bar()內(nèi)部被調(diào)用,E值被yield到外部,作為it.next(3)調(diào)用的結(jié)果。

從外部迭代器it)的角度來看,控制初始生成器和委托生成器似乎沒什么區(qū)別。

事實(shí)上,yield委托甚至沒有必要定向到另一個(gè)生成器,可以只定向到一個(gè)非生成器、通用iterable。比如:

function *bar() {
    console.log( "inside `*bar()`:", yield "A" );

    // `yield`-delegation to a non-generator!
    console.log( "inside `*bar()`:", yield *[ "B", "C", "D" ] );

    console.log( "inside `*bar()`:", yield "E" );

    return "F";
}

var it = bar();

console.log( "outside:", it.next().value );
// outside: A

console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B

console.log( "outside:", it.next( 2 ).value );
// outside: C

console.log( "outside:", it.next( 3 ).value );
// outside: D

console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: undefined
// outside: E

console.log( "outside:", it.next( 5 ).value );
// inside `*bar()`: 5
// outside: F

注意下這個(gè)例子和前一個(gè)例子中信息的接收和報(bào)告的區(qū)別。

最不可思議的是,array的默認(rèn)迭代器不關(guān)心通過next(..)調(diào)用傳入的任何信息,因此值2,34會被忽略。另外,因?yàn)槟莻€(gè)迭代器沒有顯式的return值(不像之前的*foo()),當(dāng)結(jié)束的時(shí)候,yield *表達(dá)式獲得一個(gè)undefined。

也委托異常?。‥xceptions Delegated, Too!)

yield委托兩路透明傳值一樣,錯(cuò)誤/異常也是兩路傳值的:

function *foo() {
    try {
        yield "B";
    }
    catch (err) {
        console.log( "error caught inside `*foo()`:", err );
    }

    yield "C";

    throw "D";
}

function *bar() {
    yield "A";

    try {
        yield *foo();
    }
    catch (err) {
        console.log( "error caught inside `*bar()`:", err );
    }

    yield "E";

    yield *baz();

    // note: can't get here!
    yield "G";
}

function *baz() {
    throw "F";
}

var it = bar();

console.log( "outside:", it.next().value );
// outside: A

console.log( "outside:", it.next( 1 ).value );
// outside: B

console.log( "outside:", it.throw( 2 ).value );
// error caught inside `*foo()`: 2
// outside: C

console.log( "outside:", it.next( 3 ).value );
// error caught inside `*bar()`: D
// outside: E

try {
    console.log( "outside:", it.next( 4 ).value );
}
catch (err) {
    console.log( "error caught outside:", err );
}
// error caught outside: F

這段代碼中有些東西需要注意:

  1. 當(dāng)調(diào)用it.throw(2)時(shí),會將錯(cuò)誤信息2發(fā)送給*bar(),*bar()會將其委托給*foo(),之后*foo() catch它并處理。之后yield "C"C返回作為it.throw(2)調(diào)用的返回value。
  2. *foo()內(nèi)部下一個(gè)throw拋出的"D"值傳播到*bar()中,*bar() catch它并處理。之后yield "E"返回E作為it.next(3)調(diào)用的返回value。
  3. 之后,*baz() throw出的異常沒有在*bar()中捕獲--盡管我們確實(shí)在外面catch它--因此,*baz()*bar()都被設(shè)為完成狀態(tài)。這段代碼之后,你可能無法利用隨后的next(..)調(diào)用獲得"G"值--它們只會簡單地返回undefined作為value

委托異步(Delegating Asynchrony)

讓我們回到早先的多個(gè)序列化Ajax請求的yield委托例子:

function *foo() {
    var r2 = yield request( "http://some.url.2" );
    var r3 = yield request( "http://some.url.3/?v=" + r2 );

    return r3;
}

function *bar() {
    var r1 = yield request( "http://some.url.1" );

    var r3 = yield *foo();

    console.log( r3 );
}

run( bar );

我們只是簡單地在*bar()內(nèi)部調(diào)用yield *foo(),而不是yield run(foo)

在這一例子的前一版本中,Promise機(jī)制(由run(..)控制)用來傳遞*foo()內(nèi)return r3的值給*bar()內(nèi)的局部變量r3?,F(xiàn)在,現(xiàn)在,那個(gè)值通過yield *機(jī)制直接返回。

除此之外,行為完全一致。

委托“遞歸”(Delegating "Recursion")

當(dāng)然,yield委托能夠跟蹤盡可能多的委托步驟。你甚至可以對異步的生成器“遞歸”--生成器yield委托給自己--使用yield委托:

function *foo(val) {
    if (val > 1) {
        // generator recursion
        val = yield *foo( val - 1 );
    }

    return yield request( "http://some.url/?v=" + val );
}

function *bar() {
    var r1 = yield *foo( 3 );
    console.log( r1 );
}

run( bar );

注意: run(..) utility本該以run(foo,3)的形式調(diào)用,因?yàn)樗С指郊訁?shù),用來傳遞給生成器的初始化過程。然而,這里我們用了沒有參數(shù)的*bar(),來突出yield *的靈活性。

那段代碼遵循什么執(zhí)行步驟?堅(jiān)持住,細(xì)節(jié)描述有點(diǎn)復(fù)雜:

  1. run(bar)啟動*bar()生成器。
  2. foo(3)創(chuàng)建一個(gè)*foo(3)迭代器,傳入3作為val的值。
  3. 因?yàn)?code>3>1,foo(2)創(chuàng)建了另一個(gè)迭代器,傳入2作為val的值。
  4. 因?yàn)?code>2>1,foo(1)創(chuàng)建了另一個(gè)迭代器,傳入1作為val的值。
  5. 1>1false,因此之后以值1調(diào)用request(..),獲得第一個(gè)Ajax調(diào)用返回的promise。
  6. 那個(gè)promise被yield出來,返回到*foo(2)生成器實(shí)例。
  7. yield *將那個(gè)promise傳回到*foo(3)生成器實(shí)例。另一個(gè)yield *將promise傳出給*bar()生成器實(shí)例。再一次,另一個(gè)yield *將promise傳出給run() utility,它會等待那個(gè)promise(第一個(gè)Ajax請求)解析。
  8. 當(dāng)promise解析后,它的fulfillment信息被發(fā)送出去用來恢復(fù)*bar(),信息通過yield *傳入到*foo(3)實(shí)例,之后通過yield *傳入到*foo(2)實(shí)例,之后通過yield *傳入到*foo(3)中等待的普通yield中。
  9. 現(xiàn)在,第一個(gè)調(diào)用的Ajax響應(yīng)立即從*foo(3)生成器實(shí)例中return出來,之后將其返回作為*foo(2)實(shí)例中yield *表達(dá)式的結(jié)果,并將其賦給局部變量val。
  10. *foo(2)內(nèi)部,request(..)發(fā)起了第二個(gè)Ajax請求,它的promise被yield*foo(1)實(shí)例,之后yield *將其一路傳到run(..)(重復(fù)第7步)。當(dāng)promise解析后,第二個(gè)Ajax響應(yīng)一路傳回*foo(2)生成器實(shí)例,賦給局部變量val。
  11. 最后,request(..)發(fā)起了第三個(gè)Ajax請求,它的promise返回給run(..),之后它的解析值一路返回,直至被return,以便回到*bar()中等待的yield *表達(dá)式。

唷!精神飽受摧殘了?你可能想再讀幾次,之后吃點(diǎn)零食清空下大腦!

生成器并發(fā)(Generator Concurrency)

如第一章和本章早些時(shí)候提到的,兩個(gè)同時(shí)運(yùn)行的“進(jìn)程”可以協(xié)作式的交叉各自的操作,很多時(shí)候能夠yield出強(qiáng)大的異步表達(dá)式。

坦白來講,早先的多個(gè)生成器并發(fā)交叉的例子證明了是多么的令人感到混亂。但我們暗示了某些場合,這種能力非常有用。

回想下第一章中的場景,兩個(gè)不同的同時(shí)Ajax響應(yīng)處理函數(shù)需要相互協(xié)調(diào),以便數(shù)據(jù)通信不會造成競態(tài)。我們把響應(yīng)像這樣放入到res數(shù)組中:

function response(data) {
    if (data.url == "http://some.url.1") {
        res[0] = data;
    }
    else if (data.url == "http://some.url.2") {
        res[1] = data;
    }
}

但在這一場景中,我們?nèi)绾尾l(fā)使用多個(gè)生成器呢?

// `request(..)` is a Promise-aware Ajax utility

var res = [];

function *reqData(url) {
    res.push(
        yield request( url )
    );
}

注意: 此處我們打算使用*reqData(..)的兩個(gè)生成器實(shí)例,但是和運(yùn)行兩個(gè)不同生成器的單個(gè)實(shí)例沒有什么區(qū)別;兩種方法的思考方式是一致的。一會我們看看兩個(gè)不同生成器的協(xié)調(diào)。

我們會使用協(xié)調(diào)排序,以便res.push(..)能夠?qū)⒅狄钥深A(yù)料的順序放置
,而不是手動地分為res[0]res[1]賦值。表達(dá)邏輯因此也可以更清晰些。

但實(shí)際上該如何編排這種交互呢?首先,讓我們手動用Promise實(shí)現(xiàn)一下:

var it1 = reqData( "http://some.url.1" );
var it2 = reqData( "http://some.url.2" );

var p1 = it1.next().value;
var p2 = it2.next().value;

p1
.then( function(data){
    it1.next( data );
    return p2;
} )
.then( function(data){
    it2.next( data );
} );

*reqData(..)的兩個(gè)實(shí)例都開始發(fā)起Ajax請求,之后通過yield暫停。之后當(dāng)p1解析后,我們選擇恢復(fù)第一個(gè)實(shí)例,之后p2的解析結(jié)果會重啟第二個(gè)實(shí)例。這樣的話,我們用promise編排來確保res[0]存放第一個(gè)響應(yīng),res[1]存放第二個(gè)響應(yīng)。

但坦白來講,這種方式太手動化了,并沒有真正讓生成器編排它們,這才是強(qiáng)大之處(譯者注:指讓生成器編排)。讓我們換個(gè)方式試一下:

// `request(..)` is a Promise-aware Ajax utility

var res = [];

function *reqData(url) {
    var data = yield request( url );

    // transfer control
    yield;

    res.push( data );
}

var it1 = reqData( "http://some.url.1" );
var it2 = reqData( "http://some.url.2" );

var p1 = it1.next().value;
var p2 = it2.next().value;

p1.then( function(data){
    it1.next( data );
} );

p2.then( function(data){
    it2.next( data );
} );

Promise.all( [p1,p2] )
.then( function(){
    it1.next();
    it2.next();
} );

OK,好一點(diǎn)了(盡管仍然是手動的?。?,因?yàn)楝F(xiàn)在*reqData(..)的兩個(gè)實(shí)例真的并發(fā)、獨(dú)立(至少從第一部分來講)運(yùn)行。

在上一個(gè)代碼中,直到第一個(gè)實(shí)例完全結(jié)束之后,第二個(gè)才給出它的數(shù)據(jù)。但這里,只要各自的響應(yīng)返回,兩個(gè)實(shí)例都能盡快的接收到數(shù)據(jù),之后每個(gè)實(shí)例為了控制轉(zhuǎn)移目的,作了另一個(gè)yield。之后通過在Promise.all([ .. ])的處理函數(shù)中來選擇恢復(fù)順序。

不大明顯的一點(diǎn)是,由于對稱性,這個(gè)方法暗含了一種更簡單的復(fù)用utility形式。我們可以做的更好。假設(shè)使用一個(gè)稱為runAll(..)的utility:

// `request(..)` is a Promise-aware Ajax utility

var res = [];

runAll(
    function*(){
        var p1 = request( "http://some.url.1" );

        // transfer control
        yield;

        res.push( yield p1 );
    },
    function*(){
        var p2 = request( "http://some.url.2" );

        // transfer control
        yield;

        res.push( yield p2 );
    }
);

注意: 我們沒有展示出runAll(..)的實(shí)現(xiàn)代碼,不僅因?yàn)樘L,而且還是早先實(shí)現(xiàn)的run(..)邏輯的拓展。因此,作為讀者很好的練習(xí)補(bǔ)充,試著從run(..)演化代碼,使其運(yùn)行原理和想象的runAll(..)一樣。另外,我的asynquence庫提供了之前提到了runner(..)utility,其內(nèi)建有這種功能,會在本書的附錄A中討論。

以下是runAll(..)內(nèi)部的運(yùn)行方式:

  1. 第一個(gè)生成器獲得來自"http://some.url.1"的第一個(gè)Ajax響應(yīng)的promise,之后yield控制權(quán)回到runAll(..)utility。
  2. 第二個(gè)生成器運(yùn)行,同樣處理"http://some.url.2"yield控制權(quán)回到runAll(..)utility。
  3. 第一個(gè)生成器恢復(fù),之后yield出它的promisep1,這種情況下,runAll(..)utility和之前的run(..)做的一樣,在其內(nèi)部,等待promise的解析,之后恢復(fù)同一個(gè)生成器(不是控制權(quán)轉(zhuǎn)移!)。當(dāng)p1解析后,runAll(..)用解析值再次恢復(fù)第一個(gè)生成器,之后res[0]就被賦了該值。當(dāng)?shù)谝粋€(gè)生成器結(jié)束之后,有個(gè)隱式的控制權(quán)轉(zhuǎn)移。
  4. 第二個(gè)生成器恢復(fù),yield出promisep2,等待其解析。一旦解析,runAll(..)以解析值恢復(fù)第二個(gè)生成器,并且設(shè)置res[1]

在這個(gè)運(yùn)行例子中,我們使用了外部變量res來保存兩個(gè)不同Ajax響應(yīng)的結(jié)果--這使得并發(fā)協(xié)調(diào)成為可能。

但進(jìn)一步擴(kuò)展下runAll(..),提供一個(gè)由多個(gè)生成器實(shí)例共享的內(nèi)部變量空間可能更好,比如下面我們稱為data的空對象。另外,也可以yield出非Promise變量,并把它們傳遞給下一個(gè)生成器。

考慮如下:

// `request(..)` is a Promise-aware Ajax utility

runAll(
    function*(data){
        data.res = [];

        // transfer control (and message pass)
        var url1 = yield "http://some.url.2";

        var p1 = request( url1 ); // "http://some.url.1"

        // transfer control
        yield;

        data.res.push( yield p1 );
    },
    function*(data){
        // transfer control (and message pass)
        var url2 = yield "http://some.url.1";

        var p2 = request( url2 ); // "http://some.url.2"

        // transfer control
        yield;

        data.res.push( yield p2 );
    }
);

這種形式中,兩個(gè)生成器不僅協(xié)調(diào)控制權(quán)轉(zhuǎn)移,而且還相互通信,都是通過data.res和交換rul1rul2yield出的信息。相當(dāng)強(qiáng)大!

這樣的實(shí)現(xiàn)也為一種更復(fù)雜的稱為CSP(通信序列進(jìn)程,Communicating Sequential Processes)的異步技術(shù)充當(dāng)了概念基礎(chǔ),我們會在本書的附錄B中討論。

Thunks

迄今為止,我們一直假定生成器yield出Promise--通過如run(..)的輔助utility讓Promise恢復(fù)生成器的運(yùn)行--是用生成器管理異步的最好的方法。說明白點(diǎn),它就是。

但我們跳過了另一種被廣泛接受的模式,因此,為了保證完整性,我們簡單看下。

在一般的計(jì)算機(jī)科學(xué)中,有一個(gè)很老的、在JS之前的概念,叫"thunk"。就不提它的歷史了,在JS中,thunk簡單點(diǎn)的表達(dá)就是一個(gè)調(diào)用另一個(gè)函數(shù)的函數(shù)(沒有任何參數(shù))。

換句話說,就是用函數(shù)定義包裝一個(gè)函數(shù)調(diào)用--用它所需的任何參數(shù)--來推遲調(diào)用的執(zhí)行,包裝函數(shù)就稱為一個(gè)thunk。當(dāng)之后執(zhí)行thunk時(shí),最終會調(diào)用初始的函數(shù)。

例如:

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

function fooThunk() {
    return foo( 3, 4 );
}

// later

console.log( fooThunk() );  // 7

因此,同步的thunk相當(dāng)直接。但要是異步的thunk呢?我們可以簡單地?cái)U(kuò)展thunk定義,允許接收一個(gè)回調(diào)。

考慮如下:

function foo(x,y,cb) {
    setTimeout( function(){
        cb( x + y );
    }, 1000 );
}

function fooThunk(cb) {
    foo( 3, 4, cb );
}

// later

fooThunk( function(sum){
    console.log( sum );     // 7
} );

如你所見,fooThunk(..)只期望一個(gè)cb(..)參數(shù),因?yàn)樗呀?jīng)預(yù)設(shè)有值34(分別對應(yīng)xy),并且準(zhǔn)備傳給foo(..)。thunk只需耐心等待做最后一件事:回調(diào)。

然而,你并不想手動實(shí)現(xiàn)thunk。因此,讓我們實(shí)現(xiàn)一種utility來為我們做這種包裝工作。

考慮如下:

function thunkify(fn) {
    var args = [].slice.call( arguments, 1 );
    return function(cb) {
        args.push( cb );
        return fn.apply( null, args );
    };
}

var fooThunk = thunkify( foo, 3, 4 );

// later

fooThunk( function(sum) {
    console.log( sum );     // 7
} );

提示: 此處我們假定初始函數(shù)(foo(..))希望回調(diào)處在最后的位置,其余任何參數(shù)都是在它之前。這是異步JS函數(shù)標(biāo)準(zhǔn)中相當(dāng)常見的“標(biāo)準(zhǔn)”??梢苑Q之為“callback-last style”,如果處于某種原因,你需要處理”callback-first style“的形式,只需在utility中使用args.unshift(..),而不是args.push(..)。

之前的thunkify(..)實(shí)現(xiàn)采用foo(..)函數(shù)引用和其它任何需要的參數(shù),之后返回thunk本身(fooThunk(..))。然而,這并不是JS中典型的thunk方法。

如果不是太困惑的話,thunkify(..)utility會生成一個(gè)函數(shù),該函數(shù)能生成thunks,而不是thunkify(..)直接生成thunks。

哦。。。耶。

考慮如下:

function thunkify(fn) {
    return function() {
        var args = [].slice.call( arguments );
        return function(cb) {
            args.push( cb );
            return fn.apply( null, args );
        };
    };
}

主要區(qū)別在于額外的return function() { .. }層,以下是用法的不同:

var whatIsThis = thunkify( foo );

var fooThunk = whatIsThis( 3, 4 );

// later

fooThunk( function(sum) {
    console.log( sum );     // 7
} );

很明顯,這段代碼暗含的大問題是whatIsThis如何稱呼。它不是thunk,它會生成thunk。有點(diǎn)像"thunk"“工廠”,似乎對其命名沒有個(gè)統(tǒng)一的標(biāo)準(zhǔn)。

因此,我的提議是“thunkory”(“thunk”+“factory”)。那么,thunkify(..)生成一個(gè)thunkory,之后thunkory生成thunks。原因和我第三章提議的promisory差不多:

var fooThunkory = thunkify( foo );

var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );

// later

fooThunk1( function(sum) {
    console.log( sum );     // 7
} );

fooThunk2( function(sum) {
    console.log( sum );     // 11
} );

注意: 運(yùn)行的foo(..)期望的回調(diào)形式不是“error-first style”。當(dāng)然,“error-first style”更常見。如果foo(..)預(yù)期會有某種合理的錯(cuò)誤生成,我們可以修改一下,采用錯(cuò)誤優(yōu)先回調(diào)。隨后的thunkify(..)不關(guān)心假定的是哪種形式的回調(diào)。使用方法上的唯一區(qū)別就是fooThunk1(function(err,sum){..。

暴露thunkory方法--而不是早先的thunkify(..)將中間步驟隱藏起來--似乎增加了不必要的復(fù)雜度。但通常而言,在程序開始之前,生成thunkory來包裝現(xiàn)有的API方法很有用,當(dāng)需要thunk的時(shí)候,可以傳入并調(diào)用這些thunkory。兩個(gè)分開的步驟實(shí)現(xiàn)了更清晰的功能分離。

舉例如下:

// cleaner:
var fooThunkory = thunkify( foo );

var fooThunk1 = fooThunkory( 3, 4 );
var fooThunk2 = fooThunkory( 5, 6 );

// instead of:
var fooThunk1 = thunkify( foo, 3, 4 );
var fooThunk2 = thunkify( foo, 5, 6 );

不管你喜歡顯式還是隱式地處理thunkory,thunk fooThunk1(..)fooThunk2(..)的用法仍然一樣。

s/promise/thunk/

那么,thunk如何處理生成器呢?

通常將thunk比作promise:它們不是直接可以相互取代的,因?yàn)樵谛袨樯喜⒉粚Φ取O啾扔诼惚嫉膖hunk,Promise功能更強(qiáng),更值得信任。

但從另一方面來說,它們都可以視作請求一個(gè)值,并且都是異步的。

回想下第三章中我們定義的用來promisify函數(shù)的utility,稱為Promise.wrap(..)--我們也可稱之為promisify(..)!這個(gè)Promise 包裝utility并不生成Promise;它生成promisory,promisory能夠生成Promise。這與討論的thunkory和thunk完全一致。

為了說明一致性,首先將早先的foo(..)例子改為“error-first style”回調(diào):

function foo(x,y,cb) {
    setTimeout( function(){
        // assume `cb(..)` as "error-first style"
        cb( null, x + y );
    }, 1000 );
}

現(xiàn)在,我們比較下使用thunkify(..)promisify(..)(即第三章中的Promise.wrap(..)):

// symmetrical: constructing the question asker
var fooThunkory = thunkify( foo );
var fooPromisory = promisify( foo );

// symmetrical: asking the question
var fooThunk = fooThunkory( 3, 4 );
var fooPromise = fooPromisory( 3, 4 );

// get the thunk answer
fooThunk( function(err,sum){
    if (err) {
        console.error( err );
    }
    else {
        console.log( sum );     // 7
    }
} );

// get the promise answer
fooPromise
.then(
    function(sum){
        console.log( sum );     // 7
    },
    function(err){
        console.error( err );
    }
);

本質(zhì)而言,thunkory和promisory都在問一個(gè)問題(請求值),thunk fooThunk和promise fooPromise分別代表問題的未來答案。從那個(gè)角度而言,一致性很明顯。

有這樣的想法之后,為了實(shí)現(xiàn)異步,yield Promise的生成器也可以yield thunk。我們所需的只是個(gè)更精簡的run(..) utility(和之前的差不多),不僅能夠搜尋并連接到一個(gè)yield出的Promise,而且能夠?yàn)?code>yield出的thunk提供回調(diào)函數(shù)。

考慮如下:

function *foo() {
    var val = yield request( "http://some.url.1" );
    console.log( val );
}

run( foo );

在這個(gè)例子中,request(..)既可以是個(gè)返回promise的promisory,又可以是個(gè)返回thunk的thunkory。從生成器內(nèi)部代碼邏輯角度而言,我們不關(guān)心實(shí)現(xiàn)細(xì)節(jié),這相當(dāng)強(qiáng)大!

因此,request(..)可以是這樣:

// promisory `request(..)` (see Chapter 3)
var request = Promise.wrap( ajax );

// vs.

// thunkory `request(..)`
var request = thunkify( ajax );

最后,作為早先run(..)utility的thunk式補(bǔ)丁,可能需要如下的邏輯:

// ..
// did we receive a thunk back?
else if (typeof next.value == "function") {
    return new Promise( function(resolve,reject){
        // call the thunk with an error-first callback
        next.value( function(err,msg) {
            if (err) {
                reject( err );
            }
            else {
                resolve( msg );
            }
        } );
    } )
    .then(
        handleNext,
        function handleErr(err) {
            return Promise.resolve(
                it.throw( err )
            )
            .then( handleResult );
        }
    );
}

現(xiàn)在,我們的生成器既可以調(diào)用promisory yield Promise,也可以調(diào)用thunkory yield thunk,并且無論哪一種情形,run(..)都能夠處理那個(gè)值并且使用它來等待其完成,繼而恢復(fù)生成器。

由于對稱性,這兩個(gè)方法看起來一致。然而,我們應(yīng)該指出的是,只有從Promise或者thunk代表能夠推進(jìn)生成器執(zhí)行的未來值的角度來說,這才是正確的。

從更大角度而言,thunk內(nèi)部幾乎沒有任何Promise所具備的可信任性和可組合性保證。在這種特定的生成器異步模式中,使用thunk作為Promise的替身是有效地,但相比于Promise提供的種種好處(見第三章),使用thunk應(yīng)視為不太理想的方案。

如果可以選擇,優(yōu)先用yield pr而不是yield th。但讓run(..)utility可以處理這兩種值類型沒什么問題。

提示: 我的asynquence庫中的runner(..) utility,能夠處理Promise,thunk和asynquence序列。

ES6前的生成器(Pre-ES6 Generators)

現(xiàn)在,你很希望相信生成器是異步編程工具箱中一個(gè)非常重要的添加項(xiàng)。但它是ES6中新增的語法,意味著你無法像Promise(只是一個(gè)新的API)那樣polyfill 生成器。那么如果無法忽略ES6前的瀏覽器,我們?nèi)绾螌⑸善饕霝g覽器中呢?

對于所有ES6中的語法擴(kuò)展,有些工具--最常用的術(shù)語叫轉(zhuǎn)譯器,全稱轉(zhuǎn)換-編譯--可以提供給你ES6語法,并將其轉(zhuǎn)換為對等的(但相當(dāng)丑陋)ES6前的語法。因此,生成器可以轉(zhuǎn)譯為具有同樣的行為的代碼,能夠在ES5或者更低的版本JS中運(yùn)行。

但怎么轉(zhuǎn)呢?yield“魔法”聽起來很明顯不容易轉(zhuǎn)譯。其實(shí)在早先的基于閉包的迭代器中,我們已經(jīng)暗示了一種解決方案。

手動轉(zhuǎn)換(Manual Transformation)

在我們討論轉(zhuǎn)譯器之前,讓我們研究一下手動轉(zhuǎn)譯生成器是如何工作的。這不僅僅是個(gè)學(xué)術(shù)活動,也可以幫助你增強(qiáng)對其工作原理的理解。

考慮如下:

// `request(..)` is a Promise-aware Ajax utility

function *foo(url) {
    try {
        console.log( "requesting:", url );
        var val = yield request( url );
        console.log( val );
    }
    catch (err) {
        console.log( "Oops:", err );
        return false;
    }
}

var it = foo( "http://some.url.1" );

第一點(diǎn)需要注意的是,我們?nèi)匀恍枰粋€(gè)能被調(diào)用的普通foo()函數(shù),并且仍然需要返回一個(gè)迭代器。因此,簡單勾畫一下非生成器轉(zhuǎn)譯:

function foo(url) {

    // ..

    // make and return an iterator
    return {
        next: function(v) {
            // ..
        },
        throw: function(e) {
            // ..
        }
    };
}

var it = foo( "http://some.url.1" );

接下來要關(guān)心的是生成器通過暫停它的域/狀態(tài)來實(shí)現(xiàn)其“魔法”,但我們可以用函數(shù)閉包來模擬。為理解如何寫這些代碼,我們首先用狀態(tài)值注釋一下生成器的不同部分:

// `request(..)` is a Promise-aware Ajax utility

function *foo(url) {
    // STATE *1*

    try {
        console.log( "requesting:", url );
        var TMP1 = request( url );

        // STATE *2*
        var val = yield TMP1;
        console.log( val );
    }
    catch (err) {
        // STATE *3*
        console.log( "Oops:", err );
        return false;
    }
}

注意: 為了更準(zhǔn)確地說明,我們采用臨時(shí)變量TMP1,將val = yield request..語句分成兩部分。request(..)在狀態(tài)*1*時(shí)發(fā)生,將其完成值賦給變量val在狀態(tài)*2*時(shí)發(fā)生。當(dāng)將代碼轉(zhuǎn)換為它的非生成器對等時(shí),我們需要去掉中間的TMP1

換句話說,*1*是開始狀態(tài),*2*request(..)成功狀態(tài),*3*request(..)失敗狀態(tài)。你可以想象一下附加的yield步驟是如何編碼成附加的狀態(tài)。

回到我們轉(zhuǎn)譯的生成器,讓我們在閉包中定義一個(gè)變量state,用來追蹤狀態(tài):

function foo(url) {
    // manage generator state
    var state;

    // ..
}

現(xiàn)在,在處理狀態(tài)的閉包中定義一個(gè)稱為process(..)的函數(shù),采用switch語句:

// `request(..)` is a Promise-aware Ajax utility

function foo(url) {
    // manage generator state
    var state;

    // generator-wide variable declarations
    var val;

    function process(v) {
        switch (state) {
            case 1:
                console.log( "requesting:", url );
                return request( url );
            case 2:
                val = v;
                console.log( val );
                return;
            case 3:
                var err = v;
                console.log( "Oops:", err );
                return false;
        }
    }

    // ..
}

生成器中的每個(gè)狀態(tài)對應(yīng)switch語句中的case。每次需要處理新狀態(tài)時(shí),就要調(diào)用process(..)。之后我們會講下它的工作原理。

對于任何一般的生成器變量申明(val),我們將其移至process(..)外的var聲明中,這樣可供多次process(..)調(diào)用使用。但是“塊域”變量err僅狀態(tài)*3*需要使用,因此我們將其放在塊里。

在狀態(tài)*1*時(shí),我們作了return request(..),而不是yield request(..)。在終止?fàn)顟B(tài)*2*中,沒有顯式的需要return,所以只有個(gè)簡單的return;這和return undefined一樣。在終止?fàn)顟B(tài)*3*中,有個(gè)return false,我們能夠保存該值。

現(xiàn)在我們需要在迭代器內(nèi)部定義代碼,以便能夠合適地調(diào)用process(..)

function foo(url) {
    // manage generator state
    var state;

    // generator-wide variable declarations
    var val;

    function process(v) {
        switch (state) {
            case 1:
                console.log( "requesting:", url );
                return request( url );
            case 2:
                val = v;
                console.log( val );
                return;
            case 3:
                var err = v;
                console.log( "Oops:", err );
                return false;
        }
    }

    // make and return an iterator
    return {
        next: function(v) {
            // initial state
            if (!state) {
                state = 1;
                return {
                    done: false,
                    value: process()
                };
            }
            // yield resumed successfully
            else if (state == 1) {
                state = 2;
                return {
                    done: true,
                    value: process( v )
                };
            }
            // generator already completed
            else {
                return {
                    done: true,
                    value: undefined
                };
            }
        },
        "throw": function(e) {
            // the only explicit error handling is in
            // state *1*
            if (state == 1) {
                state = 3;
                return {
                    done: true,
                    value: process( e )
                };
            }
            // otherwise, an error won't be handled,
            // so just throw it right back out
            else {
                throw e;
            }
        }
    };
}

這段代碼是如何工作的呢?

  1. 對迭代器next()的第一次調(diào)用會將生成器從未初始狀態(tài)轉(zhuǎn)到狀態(tài)1,之后調(diào)用process()來處理該狀態(tài)。request()的返回值,即Ajax的響應(yīng)promise,被返回作為next()調(diào)用的value屬性值。
  2. 如果Ajax請求成功,第二個(gè)next(..)調(diào)用需要傳入Ajax響應(yīng)值,會將狀態(tài)切換為2.process(..)被再次調(diào)用(這次需要傳入Ajax響應(yīng)值),從next(..)返回的value屬性就為undefined。
  3. 然而,如果Ajax請求失敗,應(yīng)當(dāng)以error調(diào)用throw(..),會將狀態(tài)從1變成3(而不是2)。process(..)再次被調(diào)用,這次是以錯(cuò)誤值。那個(gè)case返回false,會被設(shè)為throw(..)調(diào)用返回的value屬性值。

從外部看--它只和迭代器交互--這個(gè)foo(..)普通函數(shù)和*foo(..)表現(xiàn)的完全一樣。因此,我們已經(jīng)有效地將ES6生成器轉(zhuǎn)譯為pre-ES6的兼容代碼!

之后,我們可以手動實(shí)例化生成器并控制其迭代器--調(diào)用var it = foo("..")it.next(..),諸如此類--或者更好的方法,我們可以把它傳給之前定義的run(..)utility,即run(foo,"..")

自動轉(zhuǎn)譯(Automatic Transpilation)

之前的手動轉(zhuǎn)譯ES6生成器為pre-ES6等價(jià)代碼練習(xí)從概念上教會了我們生成器是如何工作的。但那種轉(zhuǎn)譯真的很復(fù)雜,并且不好移植到其它生成器。手動實(shí)現(xiàn)相當(dāng)不切實(shí)際,會完全消除生成器的好處。

但幸運(yùn)的是,已經(jīng)有幾個(gè)工具庫能夠?qū)S6生成器轉(zhuǎn)譯成類似我們之前轉(zhuǎn)譯的結(jié)果。它們不僅為我們做了繁重的工作,而且也處理了我們一帶而過的幾個(gè)復(fù)雜問題。

其中一個(gè)工具是regenerator(https://facebook.github.io/regenerator/),來自Facebook的小folk。

如果我們使用regenerator轉(zhuǎn)譯之前的生成器,以下是轉(zhuǎn)譯后的代碼:

// `request(..)` is a Promise-aware Ajax utility

var foo = regeneratorRuntime.mark(function foo(url) {
    var val;

    return regeneratorRuntime.wrap(function foo$(context$1$0) {
        while (1) switch (context$1$0.prev = context$1$0.next) {
        case 0:
            context$1$0.prev = 0;
            console.log( "requesting:", url );
            context$1$0.next = 4;
            return request( url );
        case 4:
            val = context$1$0.sent;
            console.log( val );
            context$1$0.next = 12;
            break;
        case 8:
            context$1$0.prev = 8;
            context$1$0.t0 = context$1$0.catch(0);
            console.log("Oops:", context$1$0.t0);
            return context$1$0.abrupt("return", false);
        case 12:
        case "end":
            return context$1$0.stop();
        }
    }, foo, this, [[0, 8]]);
});

和我們之前的手動版本相比,有一定的相似性,比如switch/case語句,我們甚至看到了從閉包中抽出的val。

當(dāng)然,有個(gè)折中,即regenerator轉(zhuǎn)譯需要一個(gè)輔助庫regeneratorRuntime,其中包含了管理通用生成器/迭代器的所有復(fù)用邏輯。許多那樣的樣版代碼看起來不同于我們的版本,但即使是那樣,也能看到相關(guān)的概念,比如用來追蹤生成器狀態(tài)的context$1$0.next = 4。

主要的挑戰(zhàn)是,生成器不僅僅限于ES6+環(huán)境中。一旦你理解了概念,就可以在代碼中使用它們,采用工具來將其轉(zhuǎn)譯成舊環(huán)境兼容的代碼。

相比pre-ES6 Promise,只需用個(gè)Promise API polyfill,這里的工作量顯然更多,但努力是完全值得的,因?yàn)樯善髂軌蛞院侠?、有意義、看起來同步、序列化的方式更好地表達(dá)異步流控制。

一旦你迷上了生成器,你就再也不想回到異步的意大利面條式的回調(diào)地獄了!

回顧(Review)

生成器是一種新的ES6函數(shù)類型,它不是像普通函數(shù)那樣運(yùn)行直至結(jié)束的。而是,生成器可以在中間過程(完全保持自身狀態(tài))暫停,并且之后可以從暫停的地方恢復(fù)。

這種暫停/恢復(fù)的切換是協(xié)作式的,而不是搶占式的。這意味著生成器有獨(dú)有的能力暫停自己(采用yield關(guān)鍵字),之后控制生成器的迭代器能夠恢復(fù)生成器(通過next(..))。

yield/next()對不僅僅是一種控制機(jī)制,實(shí)際上還是一種兩路信息傳遞機(jī)制。本質(zhì)上,yield..表達(dá)式暫停生成器并等待值,下一個(gè)next(..)調(diào)用傳回一個(gè)值(或者隱式的undefined)來恢復(fù)生成器。

生成器關(guān)于異步流控制的關(guān)鍵優(yōu)點(diǎn)是,生成器內(nèi)部的代碼能夠以同步/序列化的方式表示任務(wù)序列。技巧在于我們將異步隱藏到yield關(guān)鍵字之后了--將異步移到生成器的迭代器控制的代碼部分。

換句話說,生成器實(shí)現(xiàn)了異步代碼的序列化、同步化和阻塞性,這可以讓我們的大腦能夠更自然地推演代碼,解決基于回調(diào)的異步的兩大缺點(diǎn)之一。

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

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

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