Základy generátorů 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

Jednou z nejvíce vzrušujících nových funkcí přicházejících v JavaScript ES6 je nový druh funkcí, nazývaný generátor . Název je trochu zvláštní, ale chování se může zdát mnohem podivnější na první pohled. Tento článek si klade za cíl vysvětlit základy toho, jak fungují, a přivést vás k pochopení toho, proč jsou tak silné pro budoucnost JS.

Run-to-Completion

První věc, kterou je třeba pozorovat, když mluvíme o generátorech, je, jak se liší od normálních funkcí s ohledem na očekávání „běhu do dokončení“.

Ať už jste si to uvědomovali nebo ne, vždy jste byli schopni předpokládat něco docela zásadního o svých funkcích:jakmile se funkce spustí, bude vždy dokončena dříve, než bude možné spustit jakýkoli jiný kód JS.

Příklad:

setTimeout(function(){
    console.log("Hello World");
},1);

function foo() {
    // NOTE: don't ever do crazy long-running loops like this
    for (var i=0; i<=1E10; i++) {
        console.log(i);
    }
}

foo();
// 0..1E10
// "Hello World"

Zde je for Dokončení smyčky bude trvat poměrně dlouho, mnohem více než jednu milisekundu, ale naše zpětné volání časovače s console.log(..) příkaz nemůže přerušit foo() funkce, když běží, takže se zasekne na konci linky (na smyčce událostí) a trpělivě čeká, až na ni přijde řada.

Co když foo() dalo by se to přerušit, ale? Nezpůsobí to zmatek v našich programech?

To jsou přesně ty noční můry výzvy vícevláknového programování, ale v zemi JavaScriptu máme docela štěstí, že se o takové věci nemusíme starat, protože JS je vždy jednovláknový (v daném okamžiku se provádí pouze jeden příkaz/funkce).

Poznámka: Web Workers jsou mechanismem, ve kterém můžete spustit celé samostatné vlákno, ve kterém bude spuštěna část programu JS, zcela paralelně s hlavním vláknem programu JS. Důvod, proč to do našich programů nezavádí vícevláknové komplikace, je ten, že tato dvě vlákna spolu mohou komunikovat pouze prostřednictvím běžných asynchronních událostí, které vždy dodržují smyčku událostí jeden za druhým em> chování požadované spuštěním do dokončení.

Spustit..Zastavit..Spustit

U generátorů ES6 máme jiný druh funkce, která může být pozastavena uprostřed, jednou nebo vícekrát, a bude pokračovat později , což umožňuje spuštění jiného kódu během těchto pozastavených období.

Pokud jste někdy četli něco o souběžném nebo vláknovém programování, možná jste viděli termín „kooperativní“, který v podstatě znamená, že proces (v našem případě funkce) si sám vybírá, kdy povolí přerušení, aby mohl spolupracovat s jiným kódem. Tento koncept je v kontrastu s "preemptivním", který naznačuje, že proces/funkce by mohla být přerušena proti své vůli.

Funkce generátoru ES6 jsou „kooperativní“ ve svém souběžném chování. Uvnitř těla funkce generátoru použijete nový yield klíčové slovo pro pozastavení funkce zevnitř. Nic nemůže zastavit generátor zvenčí; zastaví se, když narazí na yield .

Jakmile však generátor má yield -sám se pozastavil, nemůže sám pokračovat. Pro restart generátoru je nutné použít externí ovládání. Za chvíli si vysvětlíme, jak k tomu dojde.

Takže v zásadě lze funkci generátoru zastavit a znovu spustit, kolikrát si vyberete. Ve skutečnosti můžete zadat funkci generátoru s nekonečnou smyčkou (jako nechvalně známý while (true) { .. } ), která v podstatě nikdy neskončí. I když je to obvykle šílenství nebo chyba v normálním JS programu, s funkcemi generátoru je to naprosto rozumné a někdy přesně to, co chcete dělat!

Ještě důležitější je, že toto zastavení a spuštění není jen ovládání provádění funkce generátoru, ale také umožňuje 2-cestné předávání zpráv do az generátoru, jak postupuje. U normálních funkcí dostanete parametry na začátku a return hodnotu na konci. S funkcemi generátoru odesíláte zprávy s každým yield a při každém restartu posíláte zprávy zpět.

Prosím o syntaxi!

Pojďme se ponořit do syntaxe těchto nových a vzrušujících funkcí generátoru.

Nejprve nová syntaxe deklarace:

function *foo() {
    // ..
}

Všimněte si * tam? To je nové a vypadá to trochu zvláštně. Pro ty z některých jiných jazyků to může vypadat strašně jako ukazatel návratové hodnoty funkce. Ale nenechte se zmást! Toto je jen způsob, jak signalizovat typ funkce speciálního generátoru.

Pravděpodobně jste viděli jiné články/dokumentaci, které používají function* foo(){ } místo function *foo(){ } (rozdíl v umístění * ). Obojí je platné, ale nedávno jsem se rozhodl, že myslím function *foo() { } je přesnější, takže to je to, co zde používám.

Nyní si promluvme o obsahu funkcí našeho generátoru. Funkce generátoru jsou ve většině ohledů jen normální funkce JS. uvnitř je k naučení velmi málo nové syntaxe funkce generátoru.

Hlavní novou hračkou, se kterou si musíme hrát, jak je uvedeno výše, je yield klíčové slovo. yield ___ se nazývá "výraz výnosu" (a ne příkaz), protože když restartujeme generátor, odešleme hodnotu zpět a cokoli odešleme, bude vypočteným výsledkem tohoto yield ___ výraz.

Příklad:

function *foo() {
    var x = 1 + (yield "foo");
    console.log(x);
}

yield "foo" výraz odešle "foo" string value out při pozastavení funkce generátoru v tomto bodě a kdykoli (pokud vůbec někdy) je generátor restartován, jakákoliv odeslaná hodnota bude výsledkem tohoto výrazu, který bude přidán do 1 a přiřazeno k x proměnná.

Vidíte obousměrnou komunikaci? Odešlete hodnotu "foo" ven, zastavte se a v určitém okamžiku později (může to být okamžitě, může to být za dlouhou dobu!), generátor se restartuje a vrátí vám hodnotu. Je to skoro jako yield klíčové slovo je druhem požadavku na hodnotu.

V libovolném umístění výrazu můžete stačí použít yield sám o sobě ve výrazu/příkazu a existuje předpokládaný undefined hodnota yield ed out. Takže:

// note: `foo(..)` here is NOT a generator!!
function foo(x) {
    console.log("x: " + x);
}

function *bar() {
    yield; // just pause
    foo( yield ); // pause waiting for a parameter to pass into `foo(..)`
}

Iterátor generátoru

"Generátor Iterátor". Docela sousto, co?

Iterátory jsou speciálním druhem chování, vlastně návrhovým vzorem, kde procházíme uspořádanou množinou hodnot jednu po druhé voláním next() . Představte si například použití iterátoru na poli, které má pět hodnot:[1,2,3,4,5] . První next() volání vrátí 1 , druhý next() volání vrátí 2 , a tak dále. Po vrácení všech hodnot next() vrátí null nebo false nebo vám jinak signalizují, že jste iterovali všechny hodnoty v datovém kontejneru.

Způsob, jakým ovládáme funkce generátoru zvenčí, je vytvořit a interagovat s iterátorem generátoru . To zní mnohem složitější, než to ve skutečnosti je. Zvažte tento hloupý příklad:

function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
}

Chcete-li procházet hodnoty tohoto *foo() generátor funkce, potřebujeme, aby byl zkonstruován iterátor. jak to uděláme? Snadno!

var it = foo();

Ach! Volání funkce generátoru normálním způsobem tedy ve skutečnosti neprovede žádný její obsah.

To je trochu zvláštní omotat si hlavu. Můžete být také v pokušení přemýšlet, proč to není var it = new foo() . Pokrčí rameny. Důvody za syntaxí jsou komplikované a mimo náš rozsah diskuse zde.

Takže teď, abychom začali iterovat naši funkci generátoru, stačí udělat:

var message = it.next();

To nám vrátí naše 1 z yield 1 prohlášení, ale to není jediná věc, kterou dostáváme zpět.

console.log(message); // { value:1, done:false }

Ve skutečnosti dostáváme zpět objekt z každého next() volání, které má value vlastnost pro yield hodnota ed-out a done je logická hodnota, která označuje, zda je funkce generátoru plně dokončena nebo ne.

Pokračujme v naší iteraci:

console.log( it.next() ); // { value:2, done:false }
console.log( it.next() ); // { value:3, done:false }
console.log( it.next() ); // { value:4, done:false }
console.log( it.next() ); // { value:5, done:false }

Zajímavé, done je stále false když dostaneme hodnotu 5 ven. Je to proto, že technicky , funkce generátoru není dokončena. Ještě musíme zavolat finální next() volání, a pokud pošleme hodnotu, musí být nastavena jako výsledek toho yield 5 výraz. Teprve pak je funkce generátoru dokončena.

Takže teď:

console.log( it.next() ); // { value:undefined, done:true }

Takže konečný výsledek naší funkce generátoru byl, že jsme funkci dokončili, ale nebyl dán žádný výsledek (protože jsme již vyčerpali všech yield ___ prohlášení).

Možná se v tuto chvíli divíte, mohu použít return z funkce generátoru, a pokud ano, odešle se tato hodnota do value majetek?

Ano ...

function *foo() {
    yield 1;
    return 2;
}

var it = foo();

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

... a ne.

Nemusí být dobrý nápad spoléhat se na return hodnotu z generátorů, protože při iteraci generátor funguje s for..of smyčky (viz níže), konečný return ed hodnota by byla zahozena.

Pro úplnost se také podívejme na odesílání zpráv do a z funkce generátoru, když ji iterujeme:

function *foo(x) {
    var y = 2 * (yield (x + 1));
    var z = yield (y / 3);
    return (x + y + z);
}

var it = foo( 5 );

// note: not sending anything into `next()` here
console.log( it.next() );       // { value:6, done:false }
console.log( it.next( 12 ) );   // { value:8, done:false }
console.log( it.next( 13 ) );   // { value:42, done:true }

Můžete vidět, že stále můžeme předávat parametry (x v našem příkladu) s počátečním foo( 5 ) iterator-instantiation volání, stejně jako u normálních funkcí, takže x být hodnota 5 .

První next(..) volejte, nic neposíláme. Proč? Protože neexistuje yield výraz přijímat to, co předáváme.

Ale pokud udělali předat hodnotu tomuto prvnímu next(..) zavolejte, nic zlého se nestane. Byla by to jen vyhozená hodnota. ES6 říká, že funkce generátoru mají v tomto případě ignorovat nepoužitou hodnotu. (Poznámka: V době psaní tohoto článku jsou noční verze Chromu i FF v pořádku, ale ostatní prohlížeče ještě nemusí být plně kompatibilní a mohou v tomto případě nesprávně vyvolat chybu).

yield (x + 1) je to, co vysílá hodnotu 6 . Druhý next(12) volání odešle 12 na to čekání yield (x + 1) výraz, tedy y je nastaven na 12 * 2 , hodnota 24 . Poté následuje yield (y / 3) (yield (24 / 3) ) je to, co vysílá hodnotu 8 . Třetí next(13) volání odešle 13 na to čekání yield (y / 3) výraz, takže z nastavte na 13 .

Nakonec return (x + y + z) je return (5 + 24 + 13) nebo 42 se vrací jako poslední value .

Přečtěte si to několikrát. Pro většinu je to divné, když to vidí poprvé několikrát.

for..of

ES6 také zahrnuje tento vzor iterátoru na syntaktické úrovni tím, že poskytuje přímou podporu pro spouštění iterátorů až do konce:for..of smyčka.

Příklad:

function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return 6;
}

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

console.log( v ); // still `5`, not `6` :(

Jak můžete vidět, iterátor vytvořil foo() je automaticky zachycen kódem for..of smyčka a automaticky se pro vás iteruje, jedna iterace pro každou hodnotu, dokud nebude done:true vyjde. Až done je false , automaticky extrahuje value vlastnost a přiřadí ji vaší iterační proměnné (v v našem případě). Jednou done je true , iterace smyčky se zastaví (a neudělá nic s žádným konečným value vráceno, pokud existuje).

Jak je uvedeno výše, můžete vidět, že for..of smyčka ignoruje a zahodí return 6 hodnota. Také proto, že zde není žádné vystavené next() volání, for..of smyčku nelze použít v situacích, kdy potřebujete předávat hodnoty do kroků generátoru, jak jsme to udělali výše.

Přehled

OK, takže to je vše pro základy generátorů. Nedělejte si starosti, pokud je to stále trochu omračující. Všichni jsme to tak zpočátku cítili!

Je přirozené se divit, co tato nová exotická hračka prakticky udělá s vaším kódem. Je toho hodně více však pro ně. Právě jsme poškrábali povrch. Takže se musíme ponořit hlouběji, než zjistíme, jak mocní mohou/budou být.

Poté, co si pohrajete s výše uvedenými úryvky kódu (vyzkoušejte Chrome nightly/canary nebo FF nightly, nebo uzel 0.11+ s --harmony flag), mohou vyvstat následující otázky:

  1. Jak funguje zpracování chyb?
  2. Může jeden generátor volat jiný generátor?
  3. Jak funguje asynchronní kódování s generátory?

Tyto a další otázky budou popsány v následujících článcích zde, takže zůstaňte naladěni!