Porozumění generátorům v jazyce JavaScript

Autor si vybral nadaci Open Internet/Free Speech Fund, které přispěl v rámci programu Write for DOnations.

Úvod

V ECMAScriptu 2015 byly do jazyka JavaScript zavedeny generátory. Generátor je proces, který lze pozastavit a obnovit a který může přinést více hodnot. Generátor v jazyce JavaScript se skládá z generátorové funkce, která vrací iterovatelný objekt Generator.

Generátory mohou udržovat stav, což poskytuje efektivní způsob vytváření iterátorů, a jsou schopny pracovat s nekonečnými datovými toky, což lze využít k implementaci nekonečného posouvání na frontend webové aplikace, k práci s daty zvukových vln a dalším. Navíc při použití s Promises mohou generátory napodobovat funkci async/await, což nám umožňuje pracovat s asynchronním kódem přímočařejším a čitelnějším způsobem. Ačkoli je async/await rozšířenějším způsobem, jak řešit běžné, jednoduché případy asynchronního použití, jako je načítání dat z API, generátory mají pokročilejší funkce, kvůli kterým se vyplatí naučit se je používat.

V tomto článku se budeme zabývat tím, jak vytvářet funkce generátoru, jak iterovat nad objekty Generator, rozdílem mezi yield a return uvnitř generátoru a dalšími aspekty práce s generátory.

Generátorové funkce

Generátorová funkce je funkce, která vrací objekt Generator a je definována klíčovým slovem function následovaným hvězdičkou (*), jak ukazuje následující obrázek:

// Generator function declarationfunction* generatorFunction() {}

Občas se hvězdička objeví vedle názvu funkce, na rozdíl od klíčového slova funkce, například function *generatorFunction(). Funguje to stejně, ale function* je rozšířenější syntaxe.

Generátorové funkce lze také definovat ve výrazu, podobně jako regulární funkce:

// Generator function expressionconst generatorFunction = function*() {}

Generátory mohou být dokonce metodami objektu nebo třídy:

V příkladech v tomto článku bude použita syntaxe deklarace generátorové funkce.

Poznámka: Na rozdíl od běžných funkcí nelze generátory konstruovat pomocí klíčového slova new, ani je nelze použít ve spojení s funkcemi šipek.

Teď, když víte, jak deklarovat generátorové funkce, podívejme se na iterovatelné Generator objekty, které vracejí.

Generátorové objekty

Tradičně funkce v JavaScriptu běží až do konce a volání funkce vrátí hodnotu, jakmile dojde ke klíčovému slovu return. Pokud je klíčové slovo return vynecháno, funkce implicitně vrátí undefined.

V následujícím kódu například deklarujeme funkci sum(), která vrací hodnotu, jež je součtem dvou celočíselných argumentů:

// A regular function that sums two valuesfunction sum(a, b) { return a + b}

Volání funkce vrací hodnotu, která je součtem argumentů:

const value = sum(5, 6) // 11

Funkce generátoru však nevrací hodnotu okamžitě a místo toho vrací iterovatelný objekt Generator. V následujícím příkladu deklarujeme funkci a dáme jí jedinou návratovou hodnotu jako standardní funkci:

// Declare a generator function with a single return valuefunction* generatorFunction() { return 'Hello, Generator!'}

Když zavoláme generátorovou funkci, vrátí objekt Generator, který můžeme přiřadit proměnné:

// Assign the Generator object to generatorconst generator = generatorFunction()

Pokud by se jednalo o běžnou funkci, očekávali bychom, že generator nám vrátí řetězec vrácený ve funkci. Ve skutečnosti však dostaneme objekt ve stavu suspended. Volání funkce generator tedy poskytne výstup podobný následujícímu:

Objekt Generator vrácený funkcí je iterátor. Iterátor je objekt, který má k dispozici metodu next(), která slouží k iteraci posloupnosti hodnot. Metoda next() vrací objekt s vlastnostmi value a done. value představuje vrácenou hodnotu a done udává, zda iterátor proběhl všemi hodnotami, nebo ne.

Víme-li toto, zavoláme next() na náš generator a zjistíme aktuální hodnotu a stav iterátoru:

// Call the next method on the Generator objectgenerator.next()

Tím získáme následující výstup:

Output
{value: "Hello, Generator!", done: true}

Vrácená hodnota z volání next() je Hello, Generator! a stav done je true, protože tato hodnota pochází z return, který iterátor uzavřel. Protože je iterátor ukončen, změní se stav funkce generátoru z suspended na closed. Opětovným voláním funkce generator získáme následující:

Output
generatorFunction {<closed>}

V tuto chvíli jsme si pouze ukázali, jak může být generátorová funkce složitějším způsobem, jak získat hodnotu return funkce. Generátorové funkce však mají také jedinečné vlastnosti, které je odlišují od běžných funkcí. V příští části se seznámíme s operátorem yield a uvidíme, jak může generátor pozastavit a obnovit provádění.

operátory yield

Generátory zavádějí do jazyka JavaScript nové klíčové slovo: yield. yield dokáže pozastavit funkci generátoru a vrátit hodnotu, která následuje po yield, čímž poskytuje lehký způsob iterace hodnot.

V tomto příkladu pozastavíme funkci generátoru třikrát s různými hodnotami a na konci vrátíme hodnotu. Pak přiřadíme náš objekt Generator do proměnné generator.

Nyní, když zavoláme next() na funkci generátoru, pozastaví se pokaždé, když narazí na yield. Po každém yield bude done nastavena na false, což znamená, že generátor ještě neskončil. Jakmile narazí na return nebo jakmile se ve funkci již nevyskytnou žádné další yield, done se překlopí na true a generátor bude ukončen.

Použijeme-li metodu next() čtyřikrát za sebou:

// Call next four timesgenerator.next()generator.next()generator.next()generator.next()

Ty dají následující čtyři řádky výstupu v pořadí:

Output
{value: "Neo", done: false}{value: "Morpheus", done: false}{value: "Trinity", done: false}{value: "The Oracle", done: true}

Všimněte si, že generátor nevyžaduje return; pokud je vynechán, poslední iterace vrátí {value: undefined, done: true}, stejně jako každé další volání next() po dokončení generátoru.

Iterace nad generátorem

Pomocí metody next() jsme ručně iterovali objekt Generator a získali všechny vlastnosti value a done celého objektu. Stejně jako Array, Map a Set se však i Generator řídí iteračním protokolem a lze jej iterovat pomocí for...of:

// Iterate over Generator objectfor (const value of generator) { console.log(value)}

To vrátí následující:

Output
NeoMorpheusTrinity

Operátor rozprostření lze také použít k přiřazení hodnot Generator do pole.

// Create an array from the values of a Generator objectconst values = console.log(values)

Tím získáme následující pole:

Output
(3)

Operátor spread i for...of nezapočítají do hodnot return (v tomto případě by to bylo 'The Oracle').

Poznámka: Zatímco obě tyto metody jsou účinné pro práci s konečnými generátory, pokud generátor pracuje s nekonečným datovým tokem, nebude možné použít přímo spread nebo for...of, aniž by se vytvořila nekonečná smyčka.

Zavření generátoru

Jak jsme viděli, generátor může mít svou vlastnost done nastavenou na true a svůj stav na closed iterací přes všechny své hodnoty. Existují dva další způsoby, jak generátor okamžitě zrušit: metodou return() a metodou throw().

Pomocí return() lze generátor ukončit v libovolném okamžiku, stejně jako kdyby byl v těle funkce příkaz return. Do return() můžete předat argument nebo jej nechat prázdný pro nedefinovanou hodnotu.

Pro demonstraci return() vytvoříme generátor s několika hodnotami yield, ale bez return v definici funkce:

function* generatorFunction() { yield 'Neo' yield 'Morpheus' yield 'Trinity'}const generator = generatorFunction()

První next() nám dá 'Neo', přičemž done nastavíme na false. Pokud hned poté vyvoláme metodu return() na objektu Generator, dostaneme nyní předanou hodnotu a done nastavenou na true. Jakékoli další volání next() poskytne výchozí dokončenou odpověď generátoru s nedefinovanou hodnotou.

Pro demonstraci tohoto postupu spusťte následující tři metody na generator:

generator.next()generator.return('There is no spoon!')generator.next()

Tím získáme následující tři výsledky:

Output
{value: "Neo", done: false}{value: "There is no spoon!", done: true}{value: undefined, done: true}

Metoda return() donutila objekt Generator dokončit a ignorovat všechna další klíčová slova yield. To je užitečné zejména při asynchronním programování, kdy potřebujete zajistit, aby bylo možné funkce zrušit, například přerušit webový požadavek, když chce uživatel provést jinou akci, protože není možné přímo zrušit slib.

Pokud má tělo funkce generátoru způsob, jak zachytit a řešit chyby, můžete použít metodu throw(), abyste do generátoru vyhodili chybu. Tím se generátor spustí, vyhodí se do něj chyba a generátor se ukončí.

Abychom si to demonstrovali, vložíme do těla funkce generátoru metodu try...catch a v případě nalezení chyby ji zaznamenáme:

Nyní spustíme metodu next() a po ní metodu throw():

generator.next()generator.throw(new Error('Agent Smith!'))

Tím získáme následující výstup:

Output
{value: "Neo", done: false}Error: Agent Smith!{value: undefined, done: true}

Pomocí throw() jsme do generátoru vložili chybu, která byla zachycena try...catch a zaznamenána do konzoly.

Metody a stavy objektu generátoru

V následující tabulce je uveden seznam metod, které lze použít na objektech Generator:

Metoda Popis
next() Vrátí další hodnotu v generátoru
return() Vrátí hodnotu v generátoru a ukončí generátor
throw() Vyhodí chybu a ukončí generátor

V další tabulce jsou uvedeny možné stavy objektu Generator:

Stav Popis
suspended Generátor zastavil provádění, ale neukončil se
closed Generátor se ukončil buď tím, že došlo k chybě, vrácením, nebo iterací všech hodnot

yield Delegation

Kromě běžného operátoru yield mohou generátory používat také výraz yield* pro delegování dalších hodnot na jiný generátor. Když na yield* narazíte uvnitř generátoru, přejde dovnitř delegovaného generátoru a začne iterovat přes všechny yield, dokud se tento generátor neuzavře. Toho lze využít k oddělení různých funkcí generátoru, abyste sémanticky uspořádali kód, a zároveň aby všechny jejich yield byly iterovatelné ve správném pořadí.

Pro demonstraci můžeme vytvořit dvě generátorové funkce, z nichž jedna bude yield* operovat s druhou:

Dále budeme iterovat přes generátorovou funkci begin():

// Iterate through the outer generatorconst generator = begin()for (const value of generator) { console.log(value)}

Tím získáme následující hodnoty v pořadí, v jakém jsou generovány:

Output
1234

Vnější generátor dal hodnoty 1 a 2, pak se delegoval na druhý generátor s yield*, který vrátil 3 a 4.

yield* může také delegovat na libovolný iterovatelný objekt, například pole nebo mapu. Delegování výnosů může být užitečné při organizaci kódu, protože každá funkce v generátoru, která by chtěla použít yield, by musela být také generátorem.

Konečné datové toky

Jedním z užitečných aspektů generátorů je možnost pracovat s nekonečnými datovými toky a kolekcemi. To lze demonstrovat vytvořením nekonečné smyčky uvnitř funkce generátoru, která zvětšuje číslo o jedničku.

V následujícím bloku kódu definujeme tuto generátorovou funkci a poté generátor spustíme:

Nyní iterujeme přes hodnoty pomocí next():

// Iterate through the valuescounter.next()counter.next()counter.next()counter.next()

Tím získáme následující výstup:

Output
{value: 0, done: false}{value: 1, done: false}{value: 2, done: false}{value: 3, done: false}

Funkce vrací postupné hodnoty v nekonečné smyčce, přičemž vlastnost done zůstává false, což zajistí, že neskončí.

S generátory se nemusíte obávat vytvoření nekonečné smyčky, protože můžete libovolně zastavit a obnovit provádění. Stále však musíte být obezřetní při způsobu vyvolání generátoru. Pokud použijete spread nebo for...of na nekonečný proud dat, budete stále iterovat přes nekonečnou smyčku najednou, což způsobí pád prostředí.

Pro složitější příklad nekonečného proudu dat můžeme vytvořit funkci generátoru Fibonacci. Fibonacciho posloupnost, která nepřetržitě sčítá dvě předchozí hodnoty dohromady, lze zapsat pomocí nekonečné smyčky v generátoru takto:

Pro vyzkoušení můžeme smyčkou projít konečné číslo a vypsat Fibonacciho posloupnost na konzolu.

// Print the first 10 values of fibonacciconst fib = fibonacci()for (let i = 0; i < 10; i++) { console.log(fib.next().value)}

Tím získáme následující:

Output
0112358132134

Schopnost pracovat s nekonečnými soubory dat je jednou z částí toho, co dělá generátory tak mocnými. To může být užitečné pro příklady, jako je implementace nekonečného rolování na frontend webové aplikace.

Předávání hodnot v generátorech

V celém tomto článku jsme používali generátory jako iterátory a v každé iteraci jsme dávali hodnoty. Kromě toho, že generátory produkují hodnoty, mohou také hodnoty spotřebovávat next(). V tomto případě bude yield obsahovat hodnotu.

Je důležité si uvědomit, že první zavolaný next() nepředá hodnotu, ale pouze spustí generátor. Abychom to demonstrovali, můžeme zaznamenat hodnotu yield a několikrát zavolat next() s nějakými hodnotami.

Tím získáme následující výstup:

Output
100200{value: "The end", done: true}

Je také možné generátor nasadit počáteční hodnotou. V následujícím příkladu vytvoříme cyklus for a každou hodnotu předáme do metody next(), ale počáteční funkci předáme také argument:

Získáme hodnotu z next() a další iteraci předáme novou hodnotu, což je předchozí hodnota krát deset. Tím získáme následující:

Output
010203040

Jiný způsob, jak se vypořádat se spuštěním generátoru, je zabalit generátor do funkce, která vždy jednou zavolá next(), než provede cokoli dalšího.

async/await s generátory

Asynchronní funkce je typ funkce dostupný v JavaScriptu ES6+, který usnadňuje práci s asynchronními daty tím, že se jeví jako synchronní. Generátory mají rozsáhlejší škálu možností než asynchronní funkce, ale jsou schopny replikovat podobné chování. Implementace asynchronního programování tímto způsobem může zvýšit flexibilitu vašeho kódu.

V této části si ukážeme příklad reprodukce async/await pomocí generátorů.

Sestavíme asynchronní funkci, která pomocí rozhraní Fetch API získá data z rozhraní JSONPlaceholder API (které poskytuje ukázková data JSON pro účely testování) a zaznamená odpověď do konzoly.

Začněte tím, že definujete asynchronní funkci s názvem getUsers, která načte data z API a vrátí pole objektů, pak zavolejte getUsers:

Tím získáte data JSON podobná následujícím:

Pomocí generátorů můžeme vytvořit něco téměř identického, co nepoužívá klíčová slova async/await. Místo toho bude používat námi vytvořenou novou funkci a hodnoty yield místo slibů await.

V následujícím bloku kódu definujeme funkci getUsers, která používá naši novou funkci asyncAlt (kterou napíšeme později), aby napodobila async/await.

Jak vidíme, vypadá to téměř identicky jako implementace async/await, až na to, že se předává funkce generátoru, která dává hodnoty.

Nyní můžeme vytvořit funkci asyncAlt, která se podobá asynchronní funkci. Funkce asyncAlt má jako parametr generátorovou funkci, což je naše funkce, která dává sliby, které vrací funkce fetch. asyncAlt sama vrací funkci a řeší každý slib, který najde, až do posledního:

Tím získáme stejný výstup jako u verze async/await:

Všimněte si, že tato implementace slouží k demonstraci toho, jak lze generátory použít místo async/await, a není to návrh připravený k výrobě. Nemá nastaveno zpracování chyb ani možnost předávat parametry do generovaných hodnot. Ačkoli tato metoda může vašemu kódu dodat flexibilitu, často bude lepší volbou async/await, protože abstrahuje od implementačních detailů a umožňuje vám soustředit se na psaní produktivního kódu.

Závěr

Generátory jsou procesy, které mohou zastavit a obnovit provádění. Jsou mocnou a všestrannou vlastností jazyka JavaScript, i když se běžně nepoužívají. V tomto kurzu jsme se seznámili s generátorovými funkcemi a objekty generátorů, metodami dostupnými generátorům, operátory yield a yield* a generátory používanými s konečnými a nekonečnými množinami dat. Prozkoumali jsme také jeden ze způsobů, jak implementovat asynchronní kód bez vnořených zpětných volání nebo dlouhých řetězců slibů.

Pokud se chcete dozvědět více o syntaxi jazyka JavaScript, podívejte se na naše výukové programy Rozumíme tomuto, Bind, Call a Apply v jazyce JavaScript a Rozumíme objektům Map a Set v jazyce JavaScript.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.