- Krátký úvod
- Generátorové funkce v JavaScriptu
- Co je to generátorová funkce?
- Vaše první generátorová funkce
- Iterable a iterátory
- Iterátory jsou rozšiřitelné a destruovatelné
- Vytahování dat z generátorové funkce
- Když se vrátíme ke generátorovým funkcím
- Případy použití generátorových funkcí
- Asynchronní generátorové funkce v JavaScriptu
- Co je to asynchronní generátorová funkce?
- Vaše první asynchronní generátorová funkce
- Asynchronní iterable a iterátory
- Výběr dat z asynchronního generátoru
- Když se vrátíme k asynchronním funkcím generátoru
- Případy použití asynchronních iterátorů a asynchronních generátorových funkcí
- Závěr
Krátký úvod
Vítejte v úžasném světě generátorových funkcí a asynchronních generátorů v JavaScriptu.
Brzy se seznámíte s jednou z nejexotičtějších (podle většiny vývojářů) funkcí JavaScriptu.
Začneme!
Generátorové funkce v JavaScriptu
Co je to generátorová funkce?
Generátorová funkce (ECMAScript 2015) v JavaScriptu je speciální typ synchronní funkce, která je schopna libovolně zastavit a obnovit své provádění.
Na rozdíl od běžných funkcí v jazyce JavaScript, které jsou fire and forget (vystřel a zapomeň), mají generátorové funkce také schopnost:
- komunikovat s volajícím prostřednictvím obousměrného kanálu.
- zachovat si kontext svého provádění (scope) i při dalších voláních.
Generátorové funkce si můžete představit jako uzávěry na steroidech, ale tím podobnost končí!
Vaše první generátorová funkce
Pro vytvoření generátorové funkce umístíme za klíčové slovo function
hvězdičku *
:
function* generate() {//}
Poznámka: generátorové funkce mohou mít také podobu metody třídy nebo funkčního výrazu. Naopak šipkové generátorové funkce nejsou povoleny.
Uvnitř funkce můžeme pomocí yield
pozastavit její provádění:
function* generate() { yield 33; yield 99;}
yield
pozastaví provádění a vrátí volajícímu tzv. objekt Generator
. Tento objekt je iterable a zároveň iterátor.
Demystifikujme si tyto pojmy.
Iterable a iterátory
Objekt iterable v jazyce JavaScript je objekt implementující Symbol.iterator
. Zde je minimální příklad iterable:
const iterable = { : function() { /* TODO: iterator */ }};
Jakmile máme tento iterable objekt, přiřadíme funkci , která vrátí objekt iterátoru.
Zní to jako spousta teorie, ale v praxi je iterable objekt, na kterém můžeme provádět cykly pomocí for...of
(ECMAScript 2015). Více o tom za chvíli.
Pár iterovatelných objektů v JavaScriptu byste už měli znát: iterovatelné jsou například pole a řetězce:
for (const char of "hello") { console.log(char);}
Další známé iterovatelné objekty jsou Map
a Set
. Pro iteraci nad hodnotami objektu se hodí také for...of
:
const person = { name: "Juliana", surname: "Crain", age: 32};for (const value of Object.values(person)) { console.log(value);}
Jen nezapomeňte, že jakákoli vlastnost označená jako enumerable: false
se v iteraci nezobrazí:
const person = { name: "Juliana", surname: "Crain", age: 32};Object.defineProperty(person, "city", { enumerable: false, value: "London"});for (const value of Object.values(person)) { console.log(value);}// Juliana// Crain// 32
Nyní je problém s naší vlastní iterovatelnou tabulkou v tom, že se sama bez iterátoru daleko nedostane.
Iterátory jsou také objekty, ale měly by vyhovovat protokolu iterátorů. Stručně řečeno, iterátory musí mít alespoň metodu next()
.
next()
musí vracet jiný objekt, jehož vlastnosti jsou value
a done
.
Logika naší metody next()
se musí řídit následujícími pravidly:
- vracíme objekt s
done: false
pro pokračování iterace. - vracíme objekt s
done: true
pro zastavení iterace.
value
místo toho by měl obsahovat výsledek, který chceme vytvořit pro konzumenta.
Rozšiřme náš příklad o iterátor:
const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};
Máme iterovatelný objekt, který správně implementuje Symbol.iterator
. Máme také iterátor, který vrací:
- objekt, jehož tvar je
{ value: x, done: false}
, dokudcount
nedosáhne 3. - objekt, jehož tvar je
{ value: x, done: true}
, kdyžcount
dosáhne 3.
Tento minimální iterovatelný objekt je připraven k iteraci pomocí for...of
:
const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};for (const iterableElement of iterable) { console.log(iterableElement);}
Výsledkem bude:
123
Jakmile se lépe seznámíte s generátorovými funkcemi, uvidíte, že iterátory jsou základem pro generátorové objekty.
Mějte na paměti:
- iterovatelné objekty jsou objekty, nad kterými iterujeme.
- iterátory jsou věci, které dělají iterovatelné … „zacyklit“ nad.
Koneckonců, jaký je smysl iterable?
Máme nyní standardní, spolehlivý for...of
cyklus, který funguje prakticky pro téměř jakoukoli datovou strukturu, vlastní nebo nativní, v jazyce JavaScript.
Chcete-li použít for...of
na vlastní datové struktuře, musíte:
- implementovat
Symbol.iterator
. - poskytnout objekt iterátoru.
To je vše!
Další zdroje:
- Iterators gonna iterate by Jake Archibald.
Iterátory jsou rozšiřitelné a destruovatelné
Kromě for...of
můžeme na konečných iterátorech používat také rozšiřování a destrukci.
Připomeňme si opět předchozí příklad:
const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};
Chceme-li vytáhnout všechny hodnoty, můžeme iterovatelnou tabulku rozložit do pole:
const values = ;console.log(values); //
Chceme-li místo toho vytáhnout jen několik hodnot, můžeme iterovatelnou tabulku destruovat. Zde získáme první a druhou hodnotu z iterovatelné tabulky:
const = iterable;console.log(first); // 1console.log(second); // 2
Tady místo toho získáme první, a třetí:
const = iterable;console.log(first); // 1console.log(third); // 3
Nyní se opět zaměříme na generátorové funkce.
Vytahování dat z generátorové funkce
Jakmile máme generátorovou funkci, můžeme s ní začít interagovat. Tato interakce spočívá v:
- získávání hodnot z generátoru, krok za krokem.
- případně odesílání hodnot zpět do generátoru.
Pro vytažení hodnot z generátoru můžeme použít tři přístupy:
- volání
next()
na objekt iterátoru. - iteraci pomocí
for...of
. - rozprostření a destrukci pole.
Funkce generátoru nevypočítá všechny své výsledky v jednom kroku, jako to dělají běžné funkce.
Podíváme-li se na náš příklad, můžeme pro získání hodnot z generátoru nejprve generátor zahřát:
function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();
Tady se go
stane naším iterovatelným/iterátorovým objektem, výsledkem volání generate
.
(Nezapomeňte, že objekt Generator
je iterable i iterátor).
Od této chvíle můžeme volat go.next()
, abychom pokročili v provádění:
function* generate() { yield 33; yield 99;}const go = generate();// Consume the generatorconst { value: firstStep } = go.next(); // firstStep is 33const { value: secondStep } = go.next(); // secondStep is 99
Zde každé volání go.next()
vytvoří nový objekt. V příkladu z tohoto objektu destruujeme vlastnost value
.
Objekty vrácené voláním next()
na objekt iterátoru mají dvě vlastnosti:
-
value
: hodnota pro aktuální krok. -
done
: boolean udávající, zda je v generátoru více hodnot, nebo ne.
Takový objekt iterátoru jsme implementovali v předchozí části. Při použití generátoru je objekt iterátoru již k dispozici.
next()
Dobře funguje pro extrakci konečných dat z objektu iterátoru.
Pro iteraci nad nekonečnými daty můžeme použít for...of
. Zde je nekonečný generátor:
function* endlessGenerator() { let counter = 0; while (true) { counter++; yield counter; }}// Consume the generatorfor (const value of endlessGenerator()) { console.log(value);}
Jak si můžete všimnout, při použití for...of
není třeba generátor inicializovat.
Nakonec můžeme objekt generátoru také rozložit a destruovat. Zde je rozprostření:
function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Spreadconst values = ;console.log(values); //
Tady je destrukce:
function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Destructuringconst = go;console.log(first); // 1console.log(second); // 2
Je důležité si uvědomit, že generátory se vyčerpají, jakmile spotřebujete všechny jejich hodnoty.
Pokud hodnoty z generátoru vyčerpáte, nezbude už nic, co byste mohli následně vytáhnout:
function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Spreadconst values = ;console.log(values); // // Exhaustconst = go;console.log(first); // undefinedconsole.log(second); // undefined
Generátory fungují i obráceně: mohou přijímat hodnoty nebo příkazy od volajícího, jak uvidíme za chvíli.
Když se vrátíme ke generátorovým funkcím
Objekty iterátoru, výsledného objektu volání generátorové funkce, vystavují následující metody:
next()
return()
throw()
Již jsme viděli next()
, která pomáhá při vytahování objektů z generátoru.
Jeho použití se neomezuje pouze na vytažení dat, ale také na odeslání hodnot do generátoru.
Podívejme se na následující generátor:
function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}
Uvádí parametr string
. Tento argument uvedeme při inicializaci:
function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");
Jakmile poprvé zavoláme next()
na objekt iterátoru, spustí se provádění a vytvoří se „A“:
function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // A
V tomto okamžiku můžeme zpětně komunikovat s generátorem zadáním argumentu pro next
:
go.next("b");
Tady je kompletní výpis:
function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // Aconsole.log(go.next("b").value); // B
Od nynějška můžeme do yield
vkládat hodnoty, kdykoli potřebujeme nový řetězec s velkými písmeny:
function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // Aconsole.log(go.next("b").value); // Bconsole.log(go.next("c").value); // Cconsole.log(go.next("d").value); // D
Pokud se kdykoli budeme chtít z provádění úplně vrátit, můžeme na objektu iterátoru použít return
:
const { value } = go.return("stop it");console.log(value); // stop it
Tím se provádění zastaví.
Kromě hodnot můžeme do generátoru hodit také výjimku. Viz „Obsluha chyb generátorových funkcí“.
Případy použití generátorových funkcí
Většina vývojářů (včetně mě) považuje generátorové funkce za exotickou vlastnost JavaScriptu, která má v reálném světě malé nebo žádné uplatnění.
To by mohla být pravda pro průměrnou práci s front-endem, kde většinou stačí posypat jQuery a trochu CSS.
Ve skutečnosti generátorové funkce skutečně září ve všech těch scénářích, kde je výkon prvořadý.
Zejména jsou dobré pro:
- práci s velkými soubory a datovými sadami.
- práci s daty na back-endu nebo ve front-endu.
- generování nekonečných posloupností dat.
- výpočet nákladné logiky na vyžádání.
Generátorové funkce jsou také stavebním kamenem pro sofistikované asynchronní vzory s asynchronními generátorovými funkcemi, což je naše téma pro příští část.
Asynchronní generátorové funkce v JavaScriptu
Co je to asynchronní generátorová funkce?
Asynchronní generátorová funkce (ECMAScript 2018) je speciální typ asynchronní funkce, která je schopna zastavit a obnovit své provádění podle libosti.
Rozdíl mezi synchronními generátorovými funkcemi a asynchronními generátorovými funkcemi spočívá v tom, že generátorové funkce vracejí asynchronní výsledek založený na slibu z objektu iterátoru.
Stejně jako generátorové funkce jsou asynchronní generátorové funkce schopny:
- komunikovat s volajícím.
- zachovat si svůj kontext provádění (scope) i při dalších voláních.
Vaše první asynchronní generátorová funkce
Pro vytvoření asynchronní generátorové funkce deklarujeme generátorovou funkci s hvězdičkou *
s prefixem async
:
async function* asyncGenerator() { //}
Uvnitř funkce můžeme pomocí yield
pozastavit její provádění:
async function* asyncGenerator() { yield 33; yield 99;}
Tady yield
pozastaví provádění a vrátí volajícímu tzv. objekt Generator
.
Tento objekt je iterable a zároveň iterátor.
Zrekapitulujme si tyto pojmy, abychom viděli, jak zapadají do asynchronní země.
Asynchronní iterable a iterátory
Asynchronní iterable v jazyce JavaScript je objekt implementující Symbol.asyncIterator
.
Tady je minimální příklad:
const asyncIterable = { : function() { /* TODO: iterator */ }};
Jakmile máme tento iterovatelný objekt, přiřadíme funkci , která vrátí objekt iterátoru.
Objekt iterátoru by měl odpovídat protokolu iterátoru s metodou next()
(stejně jako synchronní iterátor).
Rozšíříme náš příklad o iterátor:
const asyncIterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return Promise.resolve({ value: count, done: false }); } return Promise.resolve({ value: count, done: true }); } }; }};
Tento iterátor je podobný tomu, který jsme vytvořili v předchozích částech, tentokrát je jediný rozdíl v tom, že vracející objekt obalíme Promise.resolve
.
Na tomto místě můžeme udělat něco v tomto smyslu:
const go = asyncIterable();go.next().then(iterator => console.log(iterator.value));go.next().then(iterator => console.log(iterator.value));// 1// 2
Nebo pomocí for await...of
:
async function consumer() { for await (const asyncIterableElement of asyncIterable) { console.log(asyncIterableElement); }}consumer();// 1// 2// 3
Asynchronní iterátory a iterátory jsou základem asynchronních generátorových funkcí.
Na ně se nyní opět zaměříme.
Výběr dat z asynchronního generátoru
Asynchronní generátorové funkce nevypočítávají všechny své výsledky v jednom kroku, jako to dělají běžné funkce.
Na místo toho vytahujeme hodnoty postupně.
Po prozkoumání asynchronních iterátorů a iterovatelných objektů by nás nemělo překvapit, že k vytažení Promises z asynchronního generátoru můžeme použít dva přístupy:
- volání
next()
na objekt iterátoru. - asynchronní iterace pomocí
for await...of
.
V našem původním příkladu můžeme provést:
async function* asyncGenerator() { yield 33; yield 99;}const go = asyncGenerator();go.next().then(iterator => console.log(iterator.value));go.next().then(iterator => console.log(iterator.value));
Výstupem z tohoto kódu je:
3399
Druhý přístup používá asynchronní iteraci pomocí for await...of
. Pro použití asynchronní iterace obalíme konzumenta funkcí async
.
Tady je kompletní příklad:
async function* asyncGenerator() { yield 33; yield 99;}async function consumer() { for await (const value of asyncGenerator()) { console.log(value); }}consumer();
for await...of
pěkně funguje pro získávání nekonečných proudů dat.
Nyní se podíváme, jak poslat data zpět generátoru.
Když se vrátíme k asynchronním funkcím generátoru
Považte následující asynchronní generátor:
async function* asyncGenerator(string) { while (true) { string = yield string.toUpperCase(); }}
Podobně jako u nekonečného stroje s velkými písmeny z příkladu generátoru můžeme generátoru next()
poskytnout argument:
async function* asyncEndlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}async function consumer() { const go = await asyncEndlessUppercase("a"); const { value: firstStep } = await go.next(); console.log(firstStep); const { value: secondStep } = await go.next("b"); console.log(secondStep); const { value: thirdStep } = await go.next("c"); console.log(thirdStep);}consumer();
Tady každý krok pošle do generátoru novou hodnotu.
Výstupem tohoto kódu je:
ABC
I když v toUpperCase()
není ve své podstatě nic asynchronního, lze vytušit účel tohoto vzoru.
Pokud chceme kdykoli ukončit provádění, můžeme na objektu iterátoru zavolat return()
:
const { value } = await go.return("stop it"); console.log(value); // stop it
Kromě hodnot můžete do generátoru hodit také výjimku. Viz „Obsluha chyb u asynchronních generátorů“.
Případy použití asynchronních iterátorů a asynchronních generátorových funkcí
Jestliže jsou generátorové funkce dobré pro synchronní práci s velkými soubory a nekonečnými sekvencemi, asynchronní generátorové funkce umožňují pro JavaScript zcela novou zemi možností.
Zejména asynchronní iterace usnadňuje konzumaci čitelných proudů. Objekt Response
ve funkci Fetch vystavuje body
jako čitelný proud pomocí getReader()
. Takový proud můžeme obalit asynchronním generátorem a později nad ním iterovat pomocí for await...of
.
Asynchronní iterátory a generátory od Jakea Archibalda mají spoustu pěkných příkladů.
Dalšími příklady proudů jsou proudy požadavků pomocí Fetch.
V době psaní článku neexistuje žádné API prohlížeče implementující Symbol.asyncIterator
, ale specifikace Stream to změní.
V Node.js, nedávné rozhraní Stream API si dobře rozumí s asynchronními generátory a asynchronní iterací.
V budoucnu budeme moci na straně klienta bez problémů konzumovat a pracovat se zapisovatelnými, čitelnými a transformovatelnými proudy.
Další zdroje:
- The Stream API.
Závěr
Klíčové pojmy a koncepty, které jsme v tomto příspěvku probrali:
ECMAScript 2015:
- iterable.
- iterátor.
Jedná se o stavební kameny generátorových funkcí.
ECMAScript 2018:
- asynchronní iterovatel.
- asynchronní iterátor.
Tyto místo jsou stavebními kameny pro asynchronní generátorové funkce.
Dobrá znalost iterovatelů a iterátorů vám může pomoci na dlouhou cestu. Neznamená to, že s generátorovými funkcemi a asynchronními generátorovými funkcemi budete pracovat každý den, ale je dobré je mít v opasku s nástroji.
A vy? Už jste je někdy použili?
Díky za přečtení a zůstaňte s námi na tomto blogu!