Potápění hlouběji s generátory ES6

Generátory ES6:Kompletní série

  1. Základy generátorů ES6
  2. Hlubší potápění s generátory ES6
  3. Asynchronizace s generátory ES6
  4. Souběh s generátory ES6

Pokud stále ještě neznáte generátory ES6, nejprve si přečtěte a pohrajte si s kódem v části „Část 1:Základy generátorů ES6“. Jakmile si myslíte, že máte základy, nyní se můžeme ponořit do některých hlubších detailů.

Zpracování chyb

Jednou z nejvýkonnějších částí návrhu generátorů ES6 je to, že sémantika kódu uvnitř generátoru je synchronní , i když externí řízení iterace probíhá asynchronně.

To je nápaditý/složitý způsob, jak říci, že můžete použít jednoduché techniky zpracování chyb, které pravděpodobně velmi dobře znáte – konkrétně try..catch mechanismus.

Například:

function *foo() {
    try {
        var x = yield 3;
        console.log( "x: " + x ); // may never get here!
    }
    catch (err) {
        console.log( "Error: " + err );
    }
}

I když se funkce pozastaví na yield 3 výraz a mohou zůstat pozastaveny libovolně dlouho, pokud se chyba odešle zpět do generátoru, že try..catch chytí to! Zkuste to udělat s normálními asynchronními funkcemi, jako jsou zpětná volání. :)

Ale jak přesně by se chyba odeslala zpět do tohoto generátoru?

var it = foo();

var res = it.next(); // { value:3, done:false }

// instead of resuming normally with another `next(..)` call,
// let's throw a wrench (an error) into the gears:
it.throw( "Oops!" ); // Error: Oops!

Zde můžete vidět, že na iterátoru používáme jinou metodu -- throw(..) -- což "hodí" chybu do generátoru, jako by se stala přesně v místě, kde je generátor aktuálně yield - pozastaveno. try..catch zachytí tuto chybu přesně tak, jak byste očekávali!

Poznámka: Pokud throw(..) chyba v generátoru, ale ne try..catch zachytí, chyba se (stejně jako normálně) rozšíří zpět (a pokud nebude zachycena, nakonec skončí jako neošetřené odmítnutí). Takže:

function *foo() { }

var it = foo();
try {
    it.throw( "Oops!" );
}
catch (err) {
    console.log( "Error: " + err ); // Error: Oops!
}

Je zřejmé, že opačný směr zpracování chyb také funguje:

function *foo() {
    var x = yield 3;
    var y = x.toUpperCase(); // could be a TypeError error!
    yield y;
}

var it = foo();

it.next(); // { value:3, done:false }

try {
    it.next( 42 ); // `42` won't have `toUpperCase()`
}
catch (err) {
    console.log( err ); // TypeError (from `toUpperCase()` call)
}

Delegování generátorů

Další věc, kterou možná budete chtít udělat, je zavolat jiný generátor zevnitř vaší funkce generátoru. Nemyslím tím jen instanci generátoru normálním způsobem, ale vlastně delegování vaše vlastní řízení iterace do ten druhý generátor. K tomu používáme variantu yield klíčové slovo:yield * ("hvězda výtěžku").

Příklad:

function *foo() {
    yield 3;
    yield 4;
}

function *bar() {
    yield 1;
    yield 2;
    yield *foo(); // `yield *` delegates iteration control to `foo()`
    yield 5;
}

for (var v of bar()) {
    console.log( v );
}
// 1 2 3 4 5

Jak je vysvětleno v části 1 (kde jsem použil function *foo() { } místo function* foo() { } ), používám také yield *foo() zde místo yield* foo() jako mnoho jiných článků/dokumentů. Myslím, že toto je přesnější/jasnější pro ilustraci toho, co se děje.

Pojďme si rozebrat, jak to funguje. yield 1 a yield 2 odeslat jejich hodnoty přímo na for..of smyčky (skrytá) volání next() , jak již chápeme a očekáváme.

Ale pak yield* a vy si všimnete, že se podvolíme jinému generátoru tím, že jej vytvoříme (foo() ). Takže v podstatě ustupujeme/delegujeme iterátoru jiného generátoru – pravděpodobně nejpřesnější způsob, jak o tom přemýšlet.

Jednou yield* delegoval (dočasně) z *bar() na *foo() , nyní for..of smyčky next() volání ve skutečnosti řídí foo() , tedy yield 3 a yield 4 odeslat jejich hodnoty celou cestu zpět do for..of smyčka.

Jednou *foo() Po dokončení se řízení vrátí zpět k původnímu generátoru, který nakonec zavolá yield 5 .

Pro jednoduchost tento příklad pouze yield s hodnoty ven. Ale samozřejmě, pokud nepoužíváte for..of smyčku, ale stačí ručně zavolat next(..) iterátoru a předávat zprávy, tyto zprávy projdou přes yield* delegování stejným očekávaným způsobem:

function *foo() {
    var z = yield 3;
    var w = yield 4;
    console.log( "z: " + z + ", w: " + w );
}

function *bar() {
    var x = yield 1;
    var y = yield 2;
    yield *foo(); // `yield*` delegates iteration control to `foo()`
    var v = yield 5;
    console.log( "x: " + x + ", y: " + y + ", v: " + v );
}

var it = bar();

it.next();      // { value:1, done:false }
it.next( "X" ); // { value:2, done:false }
it.next( "Y" ); // { value:3, done:false }
it.next( "Z" ); // { value:4, done:false }
it.next( "W" ); // { value:5, done:false }
// z: Z, w: W

it.next( "V" ); // { value:undefined, done:true }
// x: X, y: Y, v: V

Ačkoli jsme zde ukázali pouze jednu úroveň delegování, není důvod, proč *foo() nemohl yield* delegovat na jiný iterátor generátoru a ten na jiný a tak dále.

Další "trik", že yield* můžete přijmout return ed hodnotu z delegovaného generátoru.

function *foo() {
    yield 2;
    yield 3;
    return "foo"; // return value back to `yield*` expression
}

function *bar() {
    yield 1;
    var v = yield *foo();
    console.log( "v: " + v );
    yield 4;
}

var it = bar();

it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // "v: foo"   { value:4, done:false }
it.next(); // { value:undefined, done:true }

Jak můžete vidět, yield *foo() delegoval řízení iterace (next() volání), dokud se nedokončí, a jakmile se tak stane, jakékoli return hodnota z foo() (v tomto případě hodnota řetězce "foo" ) je nastavena jako výsledná hodnota yield* výraz, který pak bude přiřazen k lokální proměnné v .

To je zajímavý rozdíl mezi yield a yield* :s yield výrazy, výsledkem je vše, co je odesláno s následným next(..) , ale s yield* výraz, obdrží svůj výsledek pouze z return delegovaného generátoru hodnotu (od next(..) odeslané hodnoty procházejí delegováním transparentně).

Můžete také provádět zpracování chyb (viz výše) v obou směrech přes yield* delegování:

function *foo() {
    try {
        yield 2;
    }
    catch (err) {
        console.log( "foo caught: " + err );
    }

    yield; // pause

    // now, throw another error
    throw "Oops!";
}

function *bar() {
    yield 1;
    try {
        yield *foo();
    }
    catch (err) {
        console.log( "bar caught: " + err );
    }
}

var it = bar();

it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }

it.throw( "Uh oh!" ); // will be caught inside `foo()`
// foo caught: Uh oh!

it.next(); // { value:undefined, done:true }  --> No error here!
// bar caught: Oops!

Jak můžete vidět, throw("Uh oh!") vyhodí chybu přes yield* delegování na try..catch uvnitř *foo() . Stejně tak throw "Oops!" uvnitř *foo() vrátí zpět na *bar() , který pak tuto chybu zachytí dalším try..catch . Kdybychom nezachytili ani jednoho z nich, chyby by se dále šířily, jak byste normálně očekávali.

Přehled

Generátory mají sémantiku synchronního provádění, což znamená, že můžete použít try..catch mechanismus zpracování chyb v yield tvrzení. Iterátor generátoru má také throw(..) způsob, jak hodit chybu do generátoru v jeho pozastavené poloze, kterou lze samozřejmě zachytit také pomocí try..catch uvnitř generátoru.

yield* umožňuje delegovat řízení iterace z aktuálního generátoru na jiný. Výsledkem je yield* funguje jako průchod v obou směrech, a to jak pro zprávy, tak pro chyby.

Jedna základní otázka však zatím zůstává nezodpovězena:jak nám generátory pomáhají se vzory asynchronního kódu? Vše, co jsme zatím viděli v těchto dvou článcích, je synchronní iterace funkcí generátoru.

Klíčem bude sestavení mechanismu, kde se generátor pozastaví, aby spustil asynchronní úlohu, a poté se obnoví (prostřednictvím iterátoru next() volání) na konci asynchronní úlohy. V příštím článku prozkoumáme různé způsoby, jak vytvořit takové řízení asynchronicity s generátory. Zůstaňte naladěni!