你不懂JS: 異步與性能 第四章: Generator(下)

官方中文版原文鏈接

感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取

Generator 委托

在上一節(jié)中,我們展示了從generator內(nèi)部調(diào)用普通函數(shù),和它如何作為一種有用的技術(shù)來將實(shí)現(xiàn)細(xì)節(jié)(比如異步Promise流程)抽象出去。但是為這樣的任務(wù)使用普通函數(shù)的缺陷是,它必須按照普通函數(shù)的規(guī)則行動(dòng),也就是說它不能像generator那樣用yield來暫停自己。

在你身上可能發(fā)生這樣的事情:你可能會(huì)試著使用我們的run(..)幫助函數(shù),從一個(gè)generator中調(diào)用另個(gè)一generator。比如:

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" );

    // 通過`run(..)`“委托”到`*foo()`
    var r3 = yield run( foo );

    console.log( r3 );
}

run( bar );

通過再一次使用我們的run(..)工具,我們?cè)?code>*bar()內(nèi)部運(yùn)行*foo()。我們利用了這樣一個(gè)事實(shí):我們?cè)缦榷x的run(..)返回一個(gè)promise,這個(gè)promise在generator運(yùn)行至完成時(shí)才解析(或發(fā)生錯(cuò)誤),所以如果我們從一個(gè)run(..)調(diào)用中yield出一個(gè)promise給另一個(gè)run(..),它就會(huì)自動(dòng)暫停*bar()直到*foo()完成。

但這里有一個(gè)更好的辦法將*foo()調(diào)用整合進(jìn)*bar(),它稱為yield委托。yield委托的特殊語法是:yield * __(注意額外的*)。讓它在我們前面的例子中工作之前,讓我們看一個(gè)更簡(jiǎn)單的場(chǎng)景:

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

注意: 在本章早前的一個(gè)注意點(diǎn)中,我解釋了為什么我偏好function *foo() ..而不是function* foo() ..,相似地,我也偏好——與關(guān)于這個(gè)話題的其他大多數(shù)文檔不同——說yield *foo()而不是yield* foo()*的擺放是純粹的風(fēng)格問題,而且要看你的最佳判斷。但我發(fā)現(xiàn)保持統(tǒng)一風(fēng)格很吸引人。

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

首先,正如我們看到過的那樣,調(diào)用foo()創(chuàng)建了一個(gè) 迭代器。然后,yield *將(當(dāng)前*bar()generator的) 迭代器 的控制委托/傳遞給這另一個(gè)*foo()迭代器

那么,前兩個(gè)it.next()調(diào)用控制著*bar(),但當(dāng)我們發(fā)起第三個(gè)it.next()調(diào)用時(shí),*foo()就啟動(dòng)了,而且這時(shí)我們控制的是*foo()而非*bar()。這就是為什么它稱為委托——*bar()將它的迭代控制委托給*foo()

只要it迭代器 的控制耗盡了整個(gè)*foo()迭代器,它就會(huì)自動(dòng)地將控制返回到*bar()

那么現(xiàn)在回到前面的三個(gè)順序Ajax請(qǐng)求的例子:

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" );

    // 通過`run(..)`“委托”到`*foo()`
    var r3 = yield *foo();

    console.log( r3 );
}

run( bar );

這個(gè)代碼段和前面使用的版本的唯一區(qū)別是,使用了yield *foo()而不是前面的yield run(foo)

注意: yield *讓出了迭代控制,不是generator控制;當(dāng)你調(diào)用*foo()generator時(shí),你就yield委托給它的 迭代器。但你實(shí)際上可以yield委托給任何 迭代器yield *[1,2,3]將會(huì)消費(fèi)默認(rèn)的[1,2,3]數(shù)組值 迭代器

為什么委托?

yield委托的目的很大程度上是為了代碼組織,而且這種方式是與普通函數(shù)調(diào)用對(duì)稱的。

想象兩個(gè)分別提供了foo()bar()方法的模塊,其中bar()調(diào)用foo()。它們倆分開的原因一般是由于為了程序?qū)⑺鼈冏鳛榉蛛x的程序來調(diào)用而進(jìn)行的恰當(dāng)組織。例如,可能會(huì)有一些情況foo()需要被獨(dú)立調(diào)用,而其他地方bar()來調(diào)用foo()

由于這些完全相同的原因,將generator分開可以增強(qiáng)程序的可讀性,可維護(hù)性,與可調(diào)試性。從這個(gè)角度講,yield *是一種快捷的語法,用來在*bar()內(nèi)部手動(dòng)地迭代*foo()的步驟。

如果*foo()中的步驟是異步的,這樣的手動(dòng)方式可能會(huì)特別復(fù)雜,這就是為什么你可能會(huì)需要那個(gè)run(..)工具來做它。正如我們已經(jīng)展示的,yield *foo()消滅了使用run(..)工具的子實(shí)例(比如run(foo))的需要。

委托消息

你可能想知道,這種yield委托在除了與 迭代器 控制一起工作以外,是如何與雙向消息傳遞一起工作的。仔細(xì)查看下面這些通過yield委托進(jìn)進(jìn)出出的消息流:

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`-委托!
    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

特別注意一下it.next(3)調(diào)用之后的處理步驟:

  1. 3被傳入(通過*bar里的yield委托)在*foo()內(nèi)部等待中的yield "C"表達(dá)式。
  2. 然后*foo()調(diào)用return "D",但是這個(gè)值不會(huì)一路返回到外面的it.next(3)調(diào)用。
  3. 相反地,值"D"作為結(jié)果被發(fā)送到在*bar()內(nèi)部等待中的yield *foo()表示式——這個(gè)yield委托表達(dá)式實(shí)質(zhì)上在*foo()被耗盡之前一直被暫停著。所以"D"被送到*bar()內(nèi)部來讓它打印。
  4. yield "E"*bar()內(nèi)部被調(diào)用,而且值"E"被讓出到外部作為it.next(3)調(diào)用的結(jié)果。

從外部 迭代器it)的角度來看,在初始的generator和被委托的generator之間的控制沒有任何區(qū)別。

事實(shí)上,yield委托甚至不必指向另一個(gè)generator;它可以僅被指向一個(gè)非generator的,一般的 iterable。比如:

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

    // `yield`-委托至一個(gè)非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)告的消息的不同之處。

最驚人的是,默認(rèn)的array迭代器 不關(guān)心任何通過next(..)調(diào)用被發(fā)送的消息,所以值23,與4實(shí)質(zhì)上被忽略了。另外,因?yàn)檫@個(gè) 迭代器 沒有明確的return值(不像前面使用的*foo()),所以yield *表達(dá)式在它完成時(shí)得到一個(gè)undefined

異常也委托!

yield委托在兩個(gè)方向上透明地傳遞消息的方式相同,錯(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. 但我們調(diào)用it.throw(2)時(shí),它發(fā)送一個(gè)錯(cuò)誤消息2*bar(),而*bar()將它委托至*foo(),然后*foo()catch它并平靜地處理。之后,yield "C""C"作為返回的value發(fā)送回it.throw(2)調(diào)用。
  2. 接下來值"D"被從*foo()內(nèi)部throw出來并傳播到*bar()*bar()會(huì)catch它并平靜地處理。然后yield "E""E"作為返回的value發(fā)送回it.next(3)調(diào)用。
  3. 接下來,一個(gè)異常從*baz()throw出來,而沒有被*bar()捕獲——我們沒在外面catch它——所以*baz()*bar()都被設(shè)置為完成狀態(tài)。這段代碼結(jié)束后,即便有后續(xù)的next(..)調(diào)用,你也不會(huì)得到值"G"——它們的value將返回undefined

異步委托

最后讓我們回到早先的多個(gè)順序Ajax請(qǐng)求的例子,使用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 run(foo)不同的是,我們調(diào)用yield *foo()就可以了。

在前一個(gè)版本的這個(gè)例子中,Promise機(jī)制(通過run(..)控制的)被用于將值從*foo()中的return r3傳送到*bar()內(nèi)部的本地變量r3。現(xiàn)在,這個(gè)值通過yield *機(jī)制直接返回。

除此以外,它們的行為是一樣的。

“遞歸”委托

當(dāng)然,yield委托可以一直持續(xù)委托下去,你想連接多少步驟就連接多少。你甚至可以在具有異步能力的generator上“遞歸”使用yield委托——一個(gè)yield委托至自己的generator:

function *foo(val) {
    if (val > 1) {
        // 遞歸委托
        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(..)工具本可以用run( foo, 3 )來調(diào)用,因?yàn)樗С钟妙~外傳遞的參數(shù)來進(jìn)行g(shù)enerator的初始化。然而,為了在這里高調(diào)展示yield *的靈活性,我們使用了無參數(shù)的*bar()

這段代碼之后的處理步驟是什么?堅(jiān)持住,它的細(xì)節(jié)要描述起來可是十分錯(cuò)綜復(fù)雜:

  1. run(bar)啟動(dòng)了*bar()generator。
  2. foo(3)*foo(..)創(chuàng)建了 迭代器 并傳遞3作為它的val參數(shù)。
  3. 因?yàn)?code>3 > 1,foo(2)創(chuàng)建了另一個(gè) 迭代器 并傳遞2作為它的val參數(shù)。
  4. 因?yàn)?code>2 > 1,foo(1)又創(chuàng)建了另一個(gè) 迭代器 并傳遞1作為它的val參數(shù)。
  5. 1 > 1false,所以我們接下來用值1調(diào)用request(..),并得到一個(gè)代表第一個(gè)Ajax調(diào)用的promise。
  6. 這個(gè)promise被yield出來,回到*foo(2)generator實(shí)例。
  7. yield *將這個(gè)promise傳出并回到*foo(3)生成generator。另一個(gè)yield *把這個(gè)promise傳出到*bar()generator實(shí)例。而又有另一個(gè)yield *把這個(gè)promise傳出到run(..)工具,而它將會(huì)等待這個(gè)promise(第一個(gè)Ajax請(qǐng)求)再處理。
  8. 當(dāng)這個(gè)promise解析時(shí),它的完成消息會(huì)被發(fā)送以繼續(xù)*bar()*bar()通過yield *把消息傳遞進(jìn)*foo(3)實(shí)例,*foo(3)實(shí)例通過yield *把消息傳遞進(jìn)*foo(2)generator實(shí)例,*foo(2)實(shí)例通過yield *把消息傳給那個(gè)在*foo(3)generator實(shí)例中等待的一般的yield
  9. 這第一個(gè)Ajax調(diào)用的應(yīng)答現(xiàn)在立即從*foo(3)generator實(shí)例中被return,作為*foo(2)實(shí)例中yield *表達(dá)式的結(jié)果發(fā)送回來,并賦值給本地val變量。
  10. *foo(2)內(nèi)部,第二個(gè)Ajax請(qǐng)求用request(..)發(fā)起,它的promise被yield回到*foo(1)實(shí)例,然后一路yield *傳播到run(..)(回到第7步)。當(dāng)promise解析時(shí),第二個(gè)Ajax應(yīng)答一路傳播回到*foo(2)generator實(shí)例,并賦值到他本地的val變量。
  11. 最終,第三個(gè)Ajax請(qǐng)求用request(..)發(fā)起,它的promise走出到run(..),然后它的解析值一路返回,最后被return到在*bar()中等待的yield *表達(dá)式。

天!許多瘋狂的頭腦雜技,對(duì)吧?你可能想要把它通讀幾遍,然后抓點(diǎn)兒零食放松一下大腦!

Generator并發(fā)

正如我們?cè)诘谝徽潞捅菊略缦扔懻撨^的,另個(gè)同時(shí)運(yùn)行的“進(jìn)程”可以協(xié)作地穿插它們的操作,而且許多時(shí)候這可以產(chǎn)生非常強(qiáng)大的異步表達(dá)式。

坦白地說,我們前面關(guān)于多個(gè)generator并發(fā)穿插的例子,展示了這真的容易讓人糊涂。但我們也受到了啟發(fā),有些地方這種能力十分有用。

回想我們?cè)诘谝徽轮锌催^的場(chǎng)景,兩個(gè)不同但同時(shí)的Ajax應(yīng)答處理需要互相協(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)绾卧谶@種場(chǎng)景下使用多generator呢?

// `request(..)` 是一個(gè)基于Promise的Ajax工具

var res = [];

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

注意: 我們將在這里使用兩個(gè)*reqData(..)generator的實(shí)例,但是這和分別使用兩個(gè)不同generator的一個(gè)實(shí)例沒有區(qū)別;這兩種方式在道理上完全一樣的。我們過一會(huì)兒就會(huì)看到兩個(gè)generator的協(xié)調(diào)操作。

與不得不將res[0]res[1]賦值手動(dòng)排序不同,我們將使用協(xié)調(diào)過的順序,讓res.push(..)以可預(yù)見的順序恰當(dāng)?shù)貙⒅捣旁陬A(yù)期的位置。如此被表達(dá)的邏輯會(huì)讓人感覺更干凈。

但是我們將如何實(shí)際安排這種互動(dòng)呢?首先,讓我們手動(dòng)實(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請(qǐng)求,然后用yield暫停。之后我們?cè)?code>p1解析時(shí)繼續(xù)運(yùn)行第一個(gè)實(shí)例,而后來的p2的解析將會(huì)重啟第二個(gè)實(shí)例。以這種方式,我們使用Promise的安排來確保res[0]將持有第一個(gè)應(yīng)答,而res[1]持有第二個(gè)應(yīng)答。

但坦白地說,這是可怕的手動(dòng),而且它沒有真正讓generator組織它們自己,而那才是真正的力量。讓我們用不同的方法試一下:

// `request(..)` 是一個(gè)基于Promise的Ajax工具

var res = [];

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

    // 傳遞控制權(quán)
    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();
} );

好的,這看起來好些了(雖然仍然是手動(dòng)),因?yàn)楝F(xiàn)在兩個(gè)*reqData(..)的實(shí)例真正地并發(fā)運(yùn)行了,而且(至少是在第一部分)是獨(dú)立的。

在前一個(gè)代碼段中,第二個(gè)實(shí)例在第一個(gè)實(shí)例完全完成之前沒有給出它的數(shù)據(jù)。但是這里,只要它們的應(yīng)答一返回這兩個(gè)實(shí)例就立即分別收到他們的數(shù)據(jù),然后每個(gè)實(shí)例調(diào)用另一個(gè)yield來傳送控制。最后我們?cè)?code>Promise.all([ .. ])的處理器中選擇用什么樣的順序繼續(xù)它們。

可能不太明顯的是,這種方式因其對(duì)稱性啟發(fā)了一種可復(fù)用工具的簡(jiǎn)單形式。讓我們想象使用一個(gè)稱為runAll(..)的工具:

// `request(..)` 是一個(gè)基于Promise的Ajax工具

var res = [];

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

        // 傳遞控制權(quán)
        yield;

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

        // 傳遞控制權(quán)
        yield;

        res.push( yield p2 );
    }
);

注意: 我們沒有包含runAll(..)的實(shí)現(xiàn)代碼,不僅因?yàn)樗L(zhǎng)得無法行文,也因?yàn)樗且粋€(gè)我們已經(jīng)在先前的 run(..)中實(shí)現(xiàn)的邏輯的擴(kuò)展。所以,作為留給讀者的一個(gè)很好的補(bǔ)充性練習(xí),請(qǐng)你自己動(dòng)手改進(jìn)run(..)的代碼,來使它像想象中的runAll(..)那樣工作。另外,我的 asynquence 庫提供了一個(gè)前面提到過的runner(..)工具,它內(nèi)建了這種能力,我們將在本書的附錄A中討論它。

這是runAll(..)內(nèi)部的處理將如何操作:

  1. 第一個(gè)generator得到一個(gè)代表從"http://some.url.1"來的Ajax應(yīng)答,然后將控制權(quán)yield回到runAll(..)工具。
  2. 第二個(gè)generator運(yùn)行,并對(duì)"http://some.url.2"做相同的事,將控制權(quán)yield回到runAll(..)工具。
  3. 第一個(gè)generator繼續(xù),然后yield出他的promisep1。在這種情況下runAll(..)工具和我們前面的run(..)做同樣的事,它等待promise解析,然后繼續(xù)這同一個(gè)generator(沒有控制傳遞!)。當(dāng)p1解析時(shí),runAll(..)使用解析值再一次繼續(xù)第一個(gè)generator,而后res[0]得到它的值。在第一個(gè)generator完成之后,有一個(gè)隱式的控制權(quán)傳遞。
  4. 第二個(gè)generator繼續(xù),yield出它的promisep2,并等待它的解析。一旦p2解析,runAll(..)使用這個(gè)解析值繼續(xù)第二個(gè)generator,于是res[1]被設(shè)置。

在這個(gè)例子中,我們使用了一個(gè)稱為res的外部變量來保存兩個(gè)不同的Ajax應(yīng)答的結(jié)果——這是我們的并發(fā)協(xié)調(diào)。

但是這樣做可能十分有幫助:進(jìn)一步擴(kuò)展runAll(..)使它為多個(gè)generator實(shí)例提供 分享的 內(nèi)部的變量作用域,比如一個(gè)我們將在下面稱為data的空對(duì)象。另外,它可以接收被yield的非Promise值,并把它們交給下一個(gè)generator。

考慮這段代碼:

// `request(..)` 是一個(gè)基于Promise的Ajax工具

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

        // 傳遞控制權(quán)(并傳遞消息)
        var url1 = yield "http://some.url.2";

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

        // 傳遞控制權(quán)
        yield;

        data.res.push( yield p1 );
    },
    function*(data){
        // 傳遞控制權(quán)(并傳遞消息)
        var url2 = yield "http://some.url.1";

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

        // 傳遞控制權(quán)
        yield;

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

在這個(gè)公式中,兩個(gè)generator不僅協(xié)調(diào)控制傳遞,實(shí)際上還互相通信:通過data.res,和交換url1url2的值的yield消息。這強(qiáng)大到不可思議!

這樣的認(rèn)識(shí)也是一種更為精巧的稱為CSP(Communicating Sequential Processes——通信順序處理)的異步技術(shù)的概念基礎(chǔ),我們將在本書的附錄B中討論它。

Thunks

至此,我們都假定從一個(gè)generator中yield一個(gè)Promise——讓這個(gè)Promise使用像run(..)這樣的幫助工具來推進(jìn)generator——是管理使用generator的異步處理的最佳方法。明白地說,它是的。

但是我們跳過了一個(gè)被輕度廣泛使用的模式,為了完整性我們將簡(jiǎn)單地看一看它。

在一般的計(jì)算機(jī)科學(xué)中,有一種老舊的前JS時(shí)代的概念,稱為“thunk”。我們不在這里贅述它的歷史,一個(gè)狹隘的表達(dá)是,thunk是一個(gè)JS函數(shù)——沒有任何參數(shù)——它連接并調(diào)用另一個(gè)函數(shù)。

換句話講,你用一個(gè)函數(shù)定義包裝函數(shù)調(diào)用——帶著它需要的所有參數(shù)——來 推遲 這個(gè)調(diào)用的執(zhí)行,而這個(gè)包裝用的函數(shù)就是thunk。當(dāng)你稍后執(zhí)行thunk時(shí),你最終會(huì)調(diào)用那個(gè)原始的函數(shù)。

舉個(gè)例子:

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

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

// 稍后

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

所以,一個(gè)同步的thunk是十分直白的。但是一個(gè)異步的thunk呢?我們實(shí)質(zhì)上可以擴(kuò)展這個(gè)狹隘的thunk定義,讓它接收一個(gè)回調(diào)。

考慮這段代碼:

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

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

// 稍后

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

如你所見,fooThunk(..)僅需要一個(gè)cb(..)參數(shù),因?yàn)樗呀?jīng)預(yù)先制定了值34(分別為xy)并準(zhǔn)備傳遞給foo(..)。一個(gè)thunk只是在外面耐心地等待著它開始工作所需的最后一部分信息:回調(diào)。

但是你不會(huì)想要手動(dòng)制造thunk。那么,讓我們發(fā)明一個(gè)工具來為我們進(jìn)行這種包裝。

考慮這段代碼:

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 );

// 稍后

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

提示: 這里我們假定原始的(foo(..))函數(shù)簽名希望它的回調(diào)的位置在最后,而其它的參數(shù)在這之前。這是一個(gè)異步JS函數(shù)的相當(dāng)普遍的“標(biāo)準(zhǔn)”。你可以稱它為“回調(diào)后置風(fēng)格”。如果因?yàn)槟承┰蚰阈枰幚怼盎卣{(diào)優(yōu)先風(fēng)格”的簽名,你只需要制造一個(gè)使用args.unshift(..)而非args.push(..)的工具。

前面的thunkify(..)公式接收foo(..)函數(shù)的引用,和任何它所需的參數(shù),并返回thunk本身(fooThunk(..))。然而,這并不是你將在JS中發(fā)現(xiàn)的thunk的典型表達(dá)方式。

thunkify(..)制造thunk本身相反,典型的——可能有點(diǎn)兒讓人困惑的——thunkify(..)工具將產(chǎn)生一個(gè)制造thunk的函數(shù)。

額...是的。

考慮這段代碼:

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

這里主要的不同之處是有一個(gè)額外的return function() { .. }。這是它在用法上的不同:

var whatIsThis = thunkify( foo );

var fooThunk = whatIsThis( 3, 4 );

// 稍后

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

明顯地,這段代碼隱含的最大的問題是,whatIsThis叫什么合適?它不是thunk,它是一個(gè)從foo(..)調(diào)用生產(chǎn)thunk的東西。它是一種“thunk”的“工廠”。而且看起來沒有任何標(biāo)準(zhǔn)的意見來命名這種東西。

所以,我的提議是“thunkory”("thunk" + "factory")。于是,thunkify(..)制造了一個(gè)thunkory,而一個(gè)thunkory制造thunks。這個(gè)道理與第三章中我的“promisory”提議是對(duì)稱的:

var fooThunkory = thunkify( foo );

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

// 稍后

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

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

注意: 這個(gè)例子中的foo(..)期望的回調(diào)不是“錯(cuò)誤優(yōu)先風(fēng)格”。當(dāng)然,“錯(cuò)誤優(yōu)先風(fēng)格”更常見。如果foo(..)有某種合理的錯(cuò)誤發(fā)生機(jī)制,我們可以改變而使它期望并使用一個(gè)錯(cuò)誤優(yōu)先的回調(diào)。后續(xù)的thunkify(..)不會(huì)關(guān)心回調(diào)被預(yù)想成什么樣。用法的唯一區(qū)別是fooThunk1(function(err,sum){..

暴露出thunkory方法——而不是像早先的thunkify(..)那樣將中間步驟隱藏起來——可能看起來像是沒必要的混亂。但是一般來講,在你的程序一開始就制造一些thunkory來包裝既存API的方法是十分有用的,然后你就可以在你需要thunk的時(shí)候傳遞并調(diào)用這些thunkory。這兩個(gè)區(qū)別開的步驟保證了功能上更干凈的分離。

來展示一下的話:

// 更干凈:
var fooThunkory = thunkify( foo );

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

// 而這個(gè)不干凈:
var fooThunk1 = thunkify( foo, 3, 4 );
var fooThunk2 = thunkify( foo, 5, 6 );

不管你是否愿意明確對(duì)付thunkory,thunk(fooThunk1(..)fooThunk2(..))的用法還是一樣的。

s/promise/thunk/

那么所有這些thunk的東西與generator有什么關(guān)系?

一般性地比較一下thunk和promise:它們是不能直接互換的,因?yàn)樗鼈冊(cè)谛袨樯喜皇堑葍r(jià)的。比起單純的thunk,Promise可用性更廣泛,而且更可靠。

但從另一種意義上講,它們都可以被看作是對(duì)一個(gè)值的請(qǐng)求,這個(gè)請(qǐng)求可能被異步地應(yīng)答。

回憶第三章,我們定義了一個(gè)工具來promise化一個(gè)函數(shù),我們稱之為Promise.wrap(..)——我們本來也可以叫它promisify(..)的!這個(gè)Promise化包裝工具不會(huì)生產(chǎn)Promise;它生產(chǎn)那些繼而生產(chǎn)Promise的promisories。這和我們當(dāng)前討論的thunkory和thunk是完全對(duì)稱的。

為了描繪這種對(duì)稱性,讓我們首先將foo(..)的例子改為假定一個(gè)“錯(cuò)誤優(yōu)先風(fēng)格”回調(diào)的形式:

function foo(x,y,cb) {
    setTimeout( function(){
        // 假定 `cb(..)` 是“錯(cuò)誤優(yōu)先風(fēng)格”
        cb( null, x + y );
    }, 1000 );
}

現(xiàn)在,我們將比較thunkify(..)promisify(..)(也就是第三章的Promise.wrap(..)):

// 對(duì)稱的:構(gòu)建問題的回答者
var fooThunkory = thunkify( foo );
var fooPromisory = promisify( foo );

// 對(duì)稱的:提出問題
var fooThunk = fooThunkory( 3, 4 );
var fooPromise = fooPromisory( 3, 4 );

// 取得 thunk 的回答
fooThunk( function(err,sum){
    if (err) {
        console.error( err );
    }
    else {
        console.log( sum );     // 7
    }
} );

// 取得 promise 的回答
fooPromise
.then(
    function(sum){
        console.log( sum );     // 7
    },
    function(err){
        console.error( err );
    }
);

thunkory和promisory實(shí)質(zhì)上都是在問一個(gè)問題(一個(gè)值),thunk的fooThunk和promise的fooPromise分別代表這個(gè)問題的未來的答案。這樣看來,對(duì)稱性就清楚了。

帶著這個(gè)視角,我們可以看到為了異步而yieldPromise的generator,也可以為異步而yieldthunk。我們需要的只是一個(gè)更聰明的run(..)工具(就像以前一樣),它不僅可以尋找并連接一個(gè)被yield的Promise,而且可以給一個(gè)被yield的thunk提供回調(diào)。

考慮這段代碼:

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

run( foo );

在這個(gè)例子中,request(..)既可以是一個(gè)返回一個(gè)promise的promisory,也可以是一個(gè)返回一個(gè)thunk的thunkory。從generator的內(nèi)部代碼邏輯的角度看,我們不關(guān)心這個(gè)實(shí)現(xiàn)細(xì)節(jié),這就它強(qiáng)大的地方!

所以,request(..)可以使以下任何一種形式:

// promisory `request(..)` (見第三章)
var request = Promise.wrap( ajax );

// vs.

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

最后,作為一個(gè)讓我們?cè)缦鹊?code>run(..)工具支持thunk的補(bǔ)丁,我們可能會(huì)需要這樣的邏輯:

// ..
// 我們收到了一個(gè)回調(diào)嗎?
else if (typeof next.value == "function") {
    return new Promise( function(resolve,reject){
        // 使用一個(gè)錯(cuò)誤優(yōu)先回調(diào)調(diào)用thunk
        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)在,我們generator既可以調(diào)用promisory來yieldPromise,也可以調(diào)用thunkory來yieldthunk,而不論那種情況,run(..)都將處理這個(gè)值并等待它的完成,以繼續(xù)generator。

在對(duì)稱性上,這兩個(gè)方式是看起來相同的。然而,我們應(yīng)當(dāng)指出這僅僅從Promise或thunk表示延續(xù)generator的未來值的角度講是成立的。

從更高的角度講,與Promise被設(shè)計(jì)成的那樣不同,thunk沒有提供,它們本身也幾乎沒有任何可靠性和可組合性的保證。在這種特定的generator異步模式下使用一個(gè)thunk作為Promise的替代品是可以工作的,但與Promise提供的所有好處相比,這應(yīng)當(dāng)被看做是一種次理想的方法。

如果你有選擇,那就偏向yield pr而非yield th。但是使run(..)工具可以處理兩種類型的值本身沒有什么問題。

注意: 在我們將要在附錄A中討論的,我的 asynquence 庫中的runner(..)工具,可以處理yield的Promise,thunk和 asynquence 序列。

前ES6時(shí)代的Generator

我希望你已經(jīng)被說服了,generator是一個(gè)異步編程工具箱里的非常重要的增強(qiáng)工具。但它是ES6中的新語法,這意味著你不能像填補(bǔ)Promise(它只是新的API)那樣填補(bǔ)generator。那么如果我們不能奢望忽略前ES6時(shí)代的瀏覽器,我們?cè)撊绾螌enerator帶到瀏覽器中呢?

對(duì)所有ES6中的新語法的擴(kuò)展,有一些工具——稱呼他們最常見的名詞是轉(zhuǎn)譯器(transpilers),也就是轉(zhuǎn)換編譯器(trans-compilers)——它們會(huì)拿起你的ES6語法,并轉(zhuǎn)換為前ES6時(shí)代的等價(jià)代碼(但是明顯地變難看了!)。所以,generator可以被轉(zhuǎn)譯為具有相同行為但可以在ES5或以下版本進(jìn)行工作的代碼。

但是怎么做到的?yield的“魔法”聽起來不像是那么容易轉(zhuǎn)譯的。在我們?cè)缦鹊幕陂]包的 迭代器 例子中,實(shí)際上提示了一種解決方法。

手動(dòng)變形

在我們討論轉(zhuǎn)譯器之前,讓我們延伸一下,在generator的情況下如何手動(dòng)轉(zhuǎn)譯。這不僅是一個(gè)學(xué)院派的練習(xí),因?yàn)檫@樣做實(shí)際上可以幫助我們進(jìn)一步理解它們?nèi)绾喂ぷ鳌?/p>

考慮這段代碼:

// `request(..)` 是一個(gè)支持Promise的Ajax工具

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" );

第一個(gè)要注意的事情是,我們?nèi)匀恍枰粋€(gè)可以被調(diào)用的普通的foo()函數(shù),而且它仍然需要返回一個(gè) 迭代器。那么讓我們來畫出非generator的變形草圖:

function foo(url) {

    // ..

    // 制造并返回 iterator
    return {
        next: function(v) {
            // ..
        },
        throw: function(e) {
            // ..
        }
    };
}

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

下一個(gè)需要注意的地方是,generator通過掛起它的作用域/狀態(tài)來施展它的“魔法”,但我們可以用函數(shù)閉包來模擬。為了理解如何寫出這樣的代碼,我們將先用狀態(tài)值注釋generator不同的部分:

// `request(..)` 是一個(gè)支持Promise的Ajax工具

function *foo(url) {
    // 狀態(tài) *1*

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

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

注意: 為了更準(zhǔn)去地講解,我們使用TMP1變量將val = yield request..語句分割為兩部分。request(..)發(fā)生在狀態(tài)*1*,而將完成值賦給val發(fā)生在狀態(tài)*2*。在我們將代碼轉(zhuǎn)換為非generator的等價(jià)物后,我們就可以擺脫中間的TMP1

換句話所,*1*是初始狀態(tài),*2*request(..)成功的狀態(tài),*3*request(..)失敗的狀態(tài)。你可能會(huì)想象額外的yield步驟將如何編碼為額外的狀態(tài)。

回到我們被轉(zhuǎn)譯的generator,讓我們?cè)谶@個(gè)閉包中定義一個(gè)變量state,用它來追蹤狀態(tài):

function foo(url) {
    // 管理 generator 狀態(tài)
    var state;

    // ..
}

現(xiàn)在,讓我們?cè)陂]包內(nèi)部定義一個(gè)稱為process(..)的內(nèi)部函數(shù),它用switch語句來處理各種狀態(tài)。

// `request(..)` 是一個(gè)支持Promise的Ajax工具

function foo(url) {
    // 管理 generator 狀態(tài)
    var state;

    // generator-范圍的變量聲明
    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;
        }
    }

    // ..
}

在我們的generator中每種狀態(tài)都在switch語句中有它自己的case。每當(dāng)我們需要處理一個(gè)新狀態(tài)時(shí),process(..)就會(huì)被調(diào)用。我們一會(huì)就回來討論它如何工作。

對(duì)任何generator范圍的變量聲明(val),我們將它們移動(dòng)到process(..)外面的var聲明中,這樣它們就可以在process(..)的多次調(diào)用中存活下來。但是“塊兒作用域”的err變量?jī)H在*3*狀態(tài)下需要,所以我們將它留在原處。

在狀態(tài)*1*,與yield request(..)相反,我們return request(..)。在終結(jié)狀態(tài)*2*,沒有明確的return,所以我們僅僅return;也就是return undefined。在終結(jié)狀態(tài)*3*,有一個(gè)return false,我們保留它。

現(xiàn)在我們需要定義 迭代器 函數(shù)的代碼,以便人們恰當(dāng)?shù)卣{(diào)用process(..)

function foo(url) {
    // 管理 generator 狀態(tài)
    var state;

    // generator-范圍的變量聲明
    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;
        }
    }

    // 制造并返回 iterator
    return {
        next: function(v) {
            // 初始狀態(tài)
            if (!state) {
                state = 1;
                return {
                    done: false,
                    value: process()
                };
            }
            // 成功地讓出繼續(xù)值
            else if (state == 1) {
                state = 2;
                return {
                    done: true,
                    value: process( v )
                };
            }
            // generator 已經(jīng)完成了
            else {
                return {
                    done: true,
                    value: undefined
                };
            }
        },
        "throw": function(e) {
            // 在狀態(tài) *1* 中,有唯一明確的錯(cuò)誤處理
            if (state == 1) {
                state = 3;
                return {
                    done: true,
                    value: process( e )
                };
            }
            // 否則,是一個(gè)不會(huì)被處理的錯(cuò)誤,所以我們僅僅把它扔回去
            else {
                throw e;
            }
        }
    };
}

這段代碼如何工作?

  1. 第一個(gè)對(duì) 迭代器next()調(diào)用將把gtenerator從未初始化的狀態(tài)移動(dòng)到狀態(tài)1,然后調(diào)用process()來處理這個(gè)狀態(tài)。request(..)的返回值是一個(gè)代表Ajax應(yīng)答的promise,它作為value屬性從next()調(diào)用被返回。
  2. 如果Ajax請(qǐng)求成功,第二個(gè)next(..)調(diào)用應(yīng)當(dāng)送進(jìn)Ajax的應(yīng)答值,它將我們的狀態(tài)移動(dòng)到2process(..)再次被調(diào)用(這次它被傳入Ajax應(yīng)答的值),而從next(..)返回的value屬性將是undefined
  3. 然而,如果Ajax請(qǐng)求失敗,應(yīng)當(dāng)用錯(cuò)誤調(diào)用throw(..),它將狀態(tài)從1移動(dòng)到3(而不是2)。process(..)再一次被調(diào)用,這詞被傳入了錯(cuò)誤的值。這個(gè)case返回false,所以false作為throw(..)調(diào)用返回的value屬性。

從外面看——也就是僅僅與 迭代器 互動(dòng)——這個(gè)普通的foo(..)函數(shù)與*foo(..)generator的工作方式是一樣的。所以我們有效地將ES6 generator“轉(zhuǎn)譯”為前ES6可兼容的!

然后我們就可以手動(dòng)初始化我們的generator并控制它的迭代器——調(diào)用var it = foo("..")it.next(..)等等——或更好地,我們可以將它傳遞給我們先前定義的run(..)工具,比如run(foo,"..")

自動(dòng)轉(zhuǎn)譯

前面的練習(xí)——手動(dòng)編寫從ES6 generator到前ES6的等價(jià)物的變形過程——教會(huì)了我們generator在概念上是如何工作的。但是這種變形真的是錯(cuò)綜復(fù)雜,而且不能很好地移植到我們代碼中的其他generator上。手動(dòng)做這些工作是不切實(shí)際的,而且將會(huì)把generator的好處完全抵消掉。

但走運(yùn)的是,已經(jīng)存在幾種工具可以自動(dòng)地將ES6 generator轉(zhuǎn)換為我們?cè)谇耙还?jié)延伸出的東西。它們不僅幫我們做力氣活兒,還可以處理幾種我們敷衍而過的情況。

一個(gè)這樣的工具是regenerator(https://facebook.github.io/regenerator/),由Facebook的聰明伙計(jì)們開發(fā)的。

如果我們用regenerator來轉(zhuǎn)譯我們前面的generator,這就是產(chǎn)生的代碼(在編寫本文時(shí)):

// `request(..)` 是一個(gè)支持Promise的Ajax工具

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]]);
});

這和我們的手動(dòng)推導(dǎo)有明顯的相似性,比如switch/case語句,而且我們甚至可以看到,val被拉到了閉包外面,正如我們做的那樣。

當(dāng)然,一個(gè)代價(jià)是這個(gè)generator的轉(zhuǎn)譯需要一個(gè)幫助工具庫regeneratorRuntime,它持有全部管理一個(gè)普通generator/迭代器 所需的可復(fù)用邏輯。它的許多模板代碼看起來和我們的版本不同,但即便如此,概念還是可以看到的,比如使用context$1$0.next = 4追蹤generator的下一個(gè)狀態(tài)。

主要的結(jié)論是,generator不僅限于ES6+的環(huán)境中才有用。一旦你理解了它的概念,你可以在你的所有代碼中利用他們,并使用工具將代碼變形為舊環(huán)境兼容的。

這比使用PromiseAPI的填補(bǔ)來實(shí)現(xiàn)前ES6的Promise要做更多的工作,但是努力完全是值得的,因?yàn)閷?duì)于以一種可推理的,合理的,看似同步的順序風(fēng)格來表達(dá)異步流程控制來說,generator實(shí)在是好太多了。

一旦你適應(yīng)了generator,你將永遠(yuǎn)不會(huì)回到面條般的回調(diào)地獄了!

復(fù)習(xí)

generator是一種ES6的新函數(shù)類型,它不像普通函數(shù)那樣運(yùn)行至完成。相反,generator可以暫停在一種中間完成狀態(tài)(完整地保留它的狀態(tài)),而且它可以從暫停的地方重新開始。

這種暫停/繼續(xù)的互換是一種協(xié)作而非搶占,這意味著generator擁有的唯一能力是使用yield關(guān)鍵字暫停它自己,而且控制這個(gè)generator的 迭代器 擁有的唯一能力是繼續(xù)這個(gè)generator(通過next(..))。

yield/next(..)的對(duì)偶不僅是一種控制機(jī)制,它實(shí)際上是一種雙向消息傳遞機(jī)制。一個(gè)yield ..表達(dá)式實(shí)質(zhì)上為了等待一個(gè)值而暫停,而下一個(gè)next(..)調(diào)用將把值(或隱含的undefined)傳遞回這個(gè)暫停的yield表達(dá)式。

與異步流程控制關(guān)聯(lián)的generator的主要好處是,在一個(gè)generator內(nèi)部的代碼以一種自然的同步/順序風(fēng)格表達(dá)一個(gè)任務(wù)的各個(gè)步驟的序列。這其中的技巧是我們實(shí)質(zhì)上將潛在的異步處理隱藏在yield關(guān)鍵字的后面——將異步處理移動(dòng)到控制generator的 迭代器 代碼中。

換句話說,generator為異步代碼保留了順序的,同步的,阻塞的代碼模式,這允許我們的大腦更自然地推理代碼,解決了基于回調(diào)的異步產(chǎn)生的兩個(gè)關(guān)鍵問題中的一個(gè)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容