Asynchronnà iterace nám umožÅuje iterovat nad daty, která pÅicházejà asynchronnÄ, na požádánÃ. NapÅÃklad když nÄco stahujeme po Äástech ze sÃtÄ. A asynchronnà generátory nám to jeÅ¡tÄ usnadÅujÃ.
Nejprve se podÃváme na jednoduchý pÅÃklad, abychom pochopili syntaxi, a pak si prohlédneme pÅÃpad použità z reálného života.
PÅipomÃnka iterovatelných objektů
PÅipomeÅme si téma iterovatelných objektů.
MyÅ¡lenkou je, že máme objekt, napÅÃklad rozsah zde:
let rozsah = {
zaÄátek: 1,
konec: 5
};
â¦A rádi bychom nad nÃm použÃvali cyklus for..of, napÅ. for(hodnota of rozsah), kterým budeme zÃskávat hodnoty od 1 do 5.
Jinými slovy, chceme pÅidat objektu schopnost iterace.
To můžeme implementovat pomocà speciálnà metody s názvem Symbol.iterator:
- Tato metoda je volána konstruktem
for..of, když je cyklus zahájen, a mÄla by vrátit objekt obsahujÃcà metodunext. - PÅi každé iteraci je metoda
next()volána pro dalšà hodnotu. - Metoda
next()by mÄla vrátit hodnotu ve tvaru{done: true/false, value:<hodnota cyklu>}, kdedone:trueznamená konec cyklu.
Zde je implementace iterovatelného objektu rozsah:
let rozsah = {
zaÄátek: 1,
konec: 5,
[Symbol.iterator]() { // voláno jednou, na zaÄátku for..of
return {
aktuálnÃ: this.zaÄátek,
poslednÃ: this.konec,
next() { // voláno pÅi každé iteraci pro zÃskánà dalšà hodnoty
if (this.aktuálnà <= this.poslednÃ) {
return { done: false, value: this.aktuálnÃ++ };
} else {
return { done: true };
}
}
};
}
};
for(let hodnota of rozsah) {
alert(hodnota); // 1, pak 2, pak 3, pak 4, pak 5
}
Pokud vám nÄco nenà jasné, prosÃme navÅ¡tivte kapitolu Iterovatelné objekty, která vysvÄtluje vÅ¡echny podrobnosti o bÄžných iterovatelných objektech.
Asynchronnà iterovatelné objekty
Asynchronnà iteraci potÅebujeme, když hodnoty pÅicházejà asynchronnÄ: po setTimeout nebo prodlevÄ jiného druhu.
NejbÄžnÄjÅ¡Ãm pÅÃpadem je, že objekt potÅebuje k doruÄenà dalšà hodnoty vytvoÅit sÃÅ¥ový požadavek. PÅÃklad z reálného života uvidÃme o nÄco pozdÄji.
Abychom uÄinili objekt asynchronnÄ iterovatelným:
- Použijeme
Symbol.asyncIteratormÃstoSymbol.iterator. - Metoda
next()by mÄla vrátit pÅÃslib (aby byl splnÄn dalšà hodnotou).- Zajistà to klÃÄové slovo
async. Můžeme jednoduÅ¡e vytvoÅitasync next().
- Zajistà to klÃÄové slovo
- K iteraci nad takovým objektem bychom mÄli použÃvat cyklus
for await (let prvek of iterovatelnýObjekt).- VÅ¡imnÄte si slova
await.
- VÅ¡imnÄte si slova
Jako poÄáteÄnà pÅÃklad vytvoÅme iterovatelný objekt rozsah. Bude podobný tomu pÅedchozÃmu, ale nynà bude vracet hodnoty asynchronnÄ, jednu za sekundu.
VÅ¡e, co musÃme udÄlat, je provést nÄkolik náhrad ve výše uvedeném kódu:
let rozsah = {
zaÄátek: 1,
konec: 5,
[Symbol.asyncIterator]() { // (1)
return {
aktuálnÃ: this.zaÄátek,
poslednÃ: this.konec,
async next() { // (2)
// poznámka: uvnitÅ asynchronnà funkce next můžeme použÃt âawaitâ:
await new Promise(splÅ => setTimeout(splÅ, 1000)); // (3)
if (this.aktuálnà <= this.poslednÃ) {
return { done: false, value: this.aktuálnÃ++ };
} else {
return { done: true };
}
}
};
}
};
(async () => {
for await (let hodnota of rozsah) { // (4)
alert(hodnota); // 1,2,3,4,5
}
})()
Jak vidÃme, tato struktura se podobá obyÄejným iterátorům:
- Abychom uÄinili objekt asynchronnÄ iterovatelným, musà obsahovat metodu
Symbol.asyncIterator(1). - Tato metoda musà vracet objekt s metodou
next(), která vracà pÅÃslib(2). - Metoda
next()nemusà býtasync, může to být bÄžná metoda vracejÃcà pÅÃslib, aleasyncnám umožÅuje použÃtawait, takže je vhodné. Zde prostÄ sekundu poÄkáme(3). - K iteraci použijeme
for await(let hodnota of rozsah)(4), jmenovitÄ pÅidáme âawaitâ za âforâ. Tento cyklus jedenkrát volározsah[Symbol.asyncIterator]()a pak pro zÃskávánà hodnot volá jehonext().
Uvedeme malou tabulku s rozdÃly:
| Iterátory | Asynchronnà iterátory | |
|---|---|---|
| Objektová metoda, která poskytne iterátor | Symbol.iterator |
Symbol.asyncIterator |
Návratová hodnota next() je |
libovolná hodnota | Promise |
| K iteraci se použÃvá | for..of |
for await..of |
... asynchronnÄ nefungujePrvky jazyka, které vyžadujà obvyklé, synchronnà iterátory, nefungujà s asynchronnÃmi.
Nebude fungovat napÅÃklad roztažená syntaxe:
alert( [...rozsah] ); // Chyba, nenà Symbol.iterator
To je pÅirozené, protože ta oÄekává Symbol.iterator, ne Symbol.asyncIterator.
To je také pÅÃpad cyklu for..of: syntaxe bez await potÅebuje Symbol.iterator.
PÅipomÃnka generátorů
Nynà si pÅipomeÅme generátory, protože ty nám umožÅujà iteraÄnà kód znaÄnÄ zkrátit. Když bychom rádi vytvoÅili iterovatelný objekt, vÄtÅ¡inou použijeme generátory.
Pro zjednoduÅ¡enÃ, když vypustÃme nÄkteré důležité vÄci, to jsou âfunkce, které generujà (vydávajÃ) hodnotyâ. Jsou podrobnÄ vysvÄtleny v kapitole Generátory.
Generátory jsou oznaÄeny jako function* (vÅ¡imnÄte si hvÄzdiÄky) a ke generovánà hodnot použÃvajà yield. Pak nad nimi můžeme procházet cyklem for..of.
Tento pÅÃklad generuje posloupnost hodnot od zaÄátek po konec:
function* generujPosloupnost(zaÄátek, konec) {
for (let i = zaÄátek; i <= konec; i++) {
yield i;
}
}
for(let hodnota of generujPosloupnost(1, 5)) {
alert(hodnota); // 1, pak 2, pak 3, pak 4, pak 5
}
Jak už vÃme, abychom uÄinili objekt iterovatelným, mÄli bychom do nÄj pÅidat Symbol.iterator.
let rozsah = {
zaÄátek: 1,
konec: 5,
[Symbol.iterator]() {
return <objekt s metodou next, aby rozsah byl iterovatelný>
}
}
BÄžnou praxà pro Symbol.iterator je vrátit generátor, tÃm se kód zkrátÃ, jak vidÃte:
let rozsah = {
zaÄátek: 1,
konec: 5,
*[Symbol.iterator]() { // zkratka pro [Symbol.iterator]: function*()
for(let hodnota = this.zaÄátek; hodnota <= this.konec; hodnota++) {
yield hodnota;
}
}
};
for(let hodnota of rozsah) {
alert(hodnota); // 1, pak 2, pak 3, pak 4, pak 5
}
Pokud by vás zajÃmaly dalšà podrobnosti, pÅeÄtÄte si prosÃme kapitolu Generátory.
V obyÄejných generátorech nemůžeme použÃvat await. VÅ¡echny hodnoty musejà pÅicházet synchronnÄ, jak vyžaduje konstrukt for..of.
Co kdybychom chtÄli generovat hodnoty asynchronnÄ, napÅÃklad ze sÃÅ¥ových požadavků?
PÅejdÄme k asynchronnÃm generátorům, které nám to umožnÃ.
Asynchronnà generátory (koneÄnÄ)
Když chceme vytvoÅit objekt, který asynchronnÄ generuje posloupnost hodnot, ve vÄtÅ¡inÄ praktických aplikacà můžeme použÃt asynchronnà generátor.
Syntaxe je jednoduchá: pÅed function* uvedeme async. To uÄinà generátor asynchronnÃm.
A pak k iteraci nad nÃm použijeme for await (...), napÅÃklad:
async function* generujPosloupnost(zaÄátek, konec) {
for (let i = zaÄátek; i <= konec; i++) {
// Hurá, můžeme použÃt await!
await new Promise(splÅ => setTimeout(splÅ, 1000));
yield i;
}
}
(async () => {
let generátor = generujPosloupnost(1, 5);
for await (let hodnota of generátor) {
alert(hodnota); // 1, pak 2, pak 3, pak 4, pak 5 (s prodlevou mezi nimi)
}
})();
Jelikož je generátor asynchronnÃ, můžeme uvnitÅ nÄj použÃvat await, spoléhat se na pÅÃsliby, provádÄt sÃÅ¥ové požadavky a podobnÄ.
Jestliže jste pokroÄilý ÄtenáŠa pamatujete si podrobnosti o generátorech, je tady technicky vnitÅnà rozdÃl.
U asynchronnÃch generátorů je metoda generátor.next() asynchronnÃ, vracà pÅÃsliby.
V obyÄejném generátoru bychom k zÃskávánà hodnot použili výsledek = generator.next(). V asynchronnÃm generátoru bychom mÄli pÅidat await, napÅÃklad takto:
výsledek = await generator.next(); // výsledek = {value: ..., done: true/false}
Z tohoto důvodu asynchronnà generátory fungujàs for await...of.
Asynchronnà iterovatelný rozsah
Jako Symbol.iterator můžeme použÃvat obyÄejné generátory, aby byl iteraÄnà kód kratÅ¡Ã.
ObdobnÄ můžeme jako Symbol.asyncIterator použÃvat asynchronnà generátory, abychom implementovali asynchronnà iteraci.
NapÅÃklad můžeme pÅimÄt objekt rozsah, aby generoval hodnoty asynchronnÄ, jednu za sekundu, nahrazenÃm synchronnÃho Symbol.iterator za asynchronnà Symbol.asyncIterator:
let rozsah = {
zaÄátek: 1,
konec: 5,
// tento Åádek je totéž jako [Symbol.asyncIterator]: async function*() {
async *[Symbol.asyncIterator]() {
for(let hodnota = this.zaÄátek; hodnota <= this.konec; hodnota++) {
// uÄiÅme pÅestávku mezi hodnotami, na nÄco Äekáme
await new Promise(splÅ => setTimeout(splÅ, 1000));
yield hodnota;
}
}
};
(async () => {
for await (let hodnota of rozsah) {
alert(hodnota); // 1, pak 2, pak 3, pak 4, pak 5
}
})();
Nynà hodnoty pÅicházejà s prodlevou 1 sekunda mezi sebou.
Technicky můžeme do objektu pÅidat Symbol.iterator i Symbol.asyncIterator souÄasnÄ, bude tedy iterovatelný jak synchronnÄ (for..of), tak asynchronnÄ (for await..of).
V praxi by to však bylo podivné.
PÅÃklad z reálného života: stránkovaná data
Dosud jsme vidÄli základnà pÅÃklady, abychom tomu porozumÄli. Nynà se podÃvejme na pÅÃpad použità z reálného života.
Existuje mnoho online služeb, které doruÄujà stránkovaná data. NapÅÃklad když potÅebujeme seznam uživatelů, požadavek vrátà pÅeddefinovaný poÄet (napÅ. 100 uživatelů) â âjednu stránkuâ a poskytne URL dalšà stránky.
Tento vzorec je zcela bÄžný. Neplatà to jen pro uživatele, ale v zásadÄ pro cokoli.
NapÅÃklad GitHub nám umožÅuje zÃskávat commity stejným způsobem, po stránkách:
- MÄli bychom vytvoÅit požadavek do
fetchve tvaruhttps://api.github.com/repos/<úložiÅ¡tÄ>/commits. - GitHub odpovà JSONem se 30 commity a také nám v hlaviÄce
Linkposkytne odkaz na dalšà stránku. - Tento odkaz pak můžeme použÃt pro dalšà požadavek, k zÃskánà dalÅ¡Ãch commitů, a tak dále.
V naÅ¡em kódu bychom rádi mÄli jednoduššà způsob, jak zÃskávat commity.
VytvoÅme funkci stáhniCommity(úložiÅ¡tÄ), která nám bude naÄÃtat commity a bude vytváÅet požadavky, kdykoli budou zapotÅebÃ. A nechme ji, aby se postarala o vÅ¡echny záležitosti ohlednÄ stránkovánÃ. Pro nás to bude jednoduchá asynchronnà iterace for await..of.
Použità tedy bude následujÃcÃ:
for await (let commit of stáhniCommity("uživatel/úložiÅ¡tÄ")) {
// zpracovánà commitu
}
Zde je taková funkce, implementovaná jako asynchronnà generátor:
async function* stáhniCommity(úložiÅ¡tÄ) {
let url = `https://api.github.com/repos/${úložiÅ¡tÄ}/commits`;
while (url) {
const odpovÄÄ = await fetch(url, { // (1)
headers: {'User-Agent': 'Our script'}, // GitHub potÅebuje hlaviÄku s uživatelským agentem
});
const tÄlo = await odpovÄÄ.json(); // (2) odpovÄÄ v JSONu (pole commitů)
// (3) v hlaviÄkách je URL dalšà stránky, vydolujeme ho
let dalÅ¡ÃStránka = odpovÄÄ.headers.get('Link').match(/<(.*?)>; rel="next"/);
dalÅ¡ÃStránka = dalÅ¡ÃStránka?.[1];
url = dalÅ¡ÃStránka;
for(let commit of tÄlo) { // (4) vydáme commity jeden po druhém, dokud stránka neskonÄÃ
yield commit;
}
}
}
PodrobnÄjšà vysvÄtlenÃ, jak to funguje:
-
K naÄtenà commitů použÃváme metodu prohlÞeÄe fetch.
- Ãvodnà URL je
https://api.github.com/repos/<úložiÅ¡tÄ>/commitsa dalšà stránka bude v hlaviÄce odpovÄdiLink. - Metoda
fetchnám umožÅuje poskytnout autorizaci a dalšà hlaviÄky, pokud jsou zapotÅebà â zde GitHub vyžaduje hlaviÄkuUser-Agent.
- Ãvodnà URL je
-
Commity jsou vráceny ve formátu JSON.
-
Z hlaviÄky
LinkodpovÄdi bychom mÄli zÃskat URL dalšà stránky. HlaviÄka má speciálnà formát, takže pro ni použijeme regulárnà výraz (o tomto prvku jazyka se dozvÃme v kapitole Regulárnà výrazy).- URL dalšà stránky může vypadat jako
https://api.github.com/repositories/93253246/commits?page=2. Generuje je samotný GitHub.
- URL dalšà stránky může vypadat jako
-
Pak budeme vydávat zÃskané commity jeden po druhém, a když dojdou, spustà se dalšà iterace
while(url), která vytvoÅà dalšà požadavek.
PÅÃklad použità (zobrazà na konzoli autory commitů):
(async () => {
let poÄet = 0;
for await (const commit of stáhniCommity('javascript-tutorial/en.javascript.info')) {
console.log(commit.author.login);
if (++poÄet == 100) { // zastavÃme se po 100 commitech
break;
}
}
})();
// Poznámka: Pokud tohle spouÅ¡tÃte v externÃm pÃskoviÅ¡ti, musÃte sem zkopÃrovat výše uvedenou funkci stáhniCommity
To je pÅesnÄ to, co jsme chtÄli.
VnitÅnà mechaniky stránkovaných požadavků jsou zvnÄjÅ¡ku neviditelné. Pro nás je to prostÄ jen asynchronnà generátor, který vracà commity.
ShrnutÃ
ObyÄejné iterátory a generátory fungujà dobÅe s daty, jejichž generovánà netrvá dlouhou dobu.
Když oÄekáváme, že data budou pÅicházet asynchronnÄ s prodlevami, můžeme použÃt jejich asynchronnà protÄjÅ¡ky a for await..of mÃsto for..of.
Syntaktické rozdÃly mezi asynchronnÃmi a bÄžnými iterátory:
| Iterovatelný objekt | Asynchronnà iterovatelný objekt | |
|---|---|---|
| Metoda poskytujÃcà iterátor | Symbol.iterator |
Symbol.asyncIterator |
Návratová hodnota next() je |
{value:â¦, done: true/false} |
Promise, který se splnà s {value:â¦, done: true/false} |
Syntaktické rozdÃly mezi asynchronnÃmi a bÄžnými generátory:
| Generátory | Asynchronnà generátory | |
|---|---|---|
| Deklarace | function* |
async function* |
Návratová hodnota next() je |
{value:â¦, done: true/false} |
Promise, který se splnà s {value:â¦, done: true/false} |
PÅi vývoji webů se Äasto setkáváme s proudy dat, která pÅitékajà po Äástech, napÅÃklad pÅi stahovánà nebo odesÃlánà velkého souboru.
Ke zpracovánà takových dat můžeme použÃvat asynchronnà generátory. Stojà také za zmÃnku, že v nÄkterých prostÅedÃch, napÅ. v prohlÞeÄÃch, existuje i dalšà API nazvané Streams, které poskytuje speciálnà rozhranà pro práci s takovými proudy (streamy), pro transformaci dat a jejich pÅedávánà z jednoho proudu do druhého (napÅ. pÅi stahovánà z jednoho mÃsta a okamžitém odesÃlánà jinam).
KomentáÅe
<code>, pro nÄkolik Åádků je obalte znaÄkou<pre>, pro vÃce než 10 Åádků vložte odkaz na pÃskoviÅ¡tÄ (plnkr, jsbin, codepenâ¦)