Asynchronní iterace nám umožňuje iterovat data, která přicházejí asynchronně, na vyžádání. Jako například když stahujeme něco kousek po kousku přes síť. A s asynchronními generátory je to ještě pohodlnější.
Podívejme se nejprve na jednoduchý příklad, abychom pochopili syntaxi, a pak si projdeme případ použití v reálném životě.
Recall iterables
Připomeňme si téma iterovatelných.
Myšlenka je taková, že máme objekt, například range
zde:
let range = {
from: 1,
to: 5
};
…A rádi bychom použili for..of
smyčka na něm, například for(value of range)
, abyste získali hodnoty z 1
na 5
.
Jinými slovy, chceme přidat schopnost iterace k objektu.
To lze implementovat pomocí speciální metody s názvem Symbol.iterator
:
- Tato metoda je volána pomocí
for..of
konstrukt při spuštění smyčky a měl by vrátit objekt snext
metoda. - Pro každou iteraci
next()
metoda je vyvolána pro další hodnotu. next()
by měl vrátit hodnotu ve tvaru{done: true/false, value:<loop value>}
, kdedone:true
znamená konec smyčky.
Zde je implementace iterovatelného range
:
let range = {
from: 1,
to: 5,
[Symbol.iterator]() { // called once, in the beginning of for..of
return {
current: this.from,
last: this.to,
next() { // called every iteration, to get the next value
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
for(let value of range) {
alert(value); // 1 then 2, then 3, then 4, then 5
}
Pokud je něco nejasné, navštivte prosím kapitolu Iterables, kde jsou uvedeny všechny podrobnosti o běžných iterablech.
Asynchronní iterovatelné
Asynchronní iterace je nutná, když hodnoty přicházejí asynchronně:po setTimeout
nebo jiný druh zpoždění.
Nejběžnějším případem je, že objekt potřebuje provést síťový požadavek, aby dodal další hodnotu, o něco později uvidíme skutečný příklad.
Chcete-li, aby byl objekt iterovatelný asynchronně:
- Použijte
Symbol.asyncIterator
místoSymbol.iterator
. next()
metoda by měla vrátit slib (který má být splněn s další hodnotou).async
klíčové slovo to zvládne, můžeme jednoduše vytvořitasync next()
.
- Pro iteraci takového objektu bychom měli použít
for await (let item of iterable)
smyčka.- Všimněte si
await
slovo.
- Všimněte si
Jako výchozí příklad udělejme iterovatelný range
objekt, podobný jako ten předtím, ale nyní bude vracet hodnoty asynchronně, jednu za sekundu.
Vše, co musíme udělat, je provést několik nahrazení v kódu výše:
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() { // (1)
return {
current: this.from,
last: this.to,
async next() { // (2)
// note: we can use "await" inside the async next:
await new Promise(resolve => setTimeout(resolve, 1000)); // (3)
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
(async () => {
for await (let value of range) { // (4)
alert(value); // 1,2,3,4,5
}
})()
Jak vidíme, struktura je podobná běžným iterátorům:
- Aby objekt mohl být asynchronně iterovatelný, musí mít metodu
Symbol.asyncIterator
(1)
. - Tato metoda musí vrátit objekt s
next()
metoda vracející slib(2)
. next()
metoda nemusí býtasync
, může to být běžná metoda vracející slib, aleasync
nám umožňuje používatawait
, takže je to pohodlné. Zde jen zdržíme na sekundu(3)
.- K iteraci používáme
for await(let value of range)
(4)
, jmenovitě přidat „čekat“ za „pro“. Volárange[Symbol.asyncIterator]()
jednou a poté jehonext()
pro hodnoty.
Zde je malá tabulka s rozdíly:
Iterátory | Asynchronní iterátory | |
---|---|---|
Metoda objektu pro poskytnutí iterátoru | Symbol.iterator | Symbol.asyncIterator |
next() návratová hodnota je | jakákoli hodnota | Promise |
pro zacyklení použijte | for..of | for await..of |
...
nefunguje asynchronně Funkce, které vyžadují pravidelné synchronní iterátory, nefungují s asynchronními.
Například syntaxe spreadu nebude fungovat:
alert( [...range] ); // Error, no Symbol.iterator
To je přirozené, protože očekává, že najde Symbol.iterator
, nikoli Symbol.asyncIterator
.
To je také případ for..of
:syntaxe bez await
potřebuje Symbol.iterator
.
Generátory vyvolání
Nyní si připomeňme generátory, protože umožňují mnohem kratší iterační kód. Většinou, když bychom chtěli udělat iterovatelný, použijeme generátory.
Pro čistou jednoduchost, vynechání některých důležitých věcí, jsou to „funkce, které generují (výnosové) hodnoty“. Jsou podrobně vysvětleny v kapitole Generátory.
Generátory jsou označeny function*
(všimněte si hvězdičky) a použijte yield
pro vygenerování hodnoty pak můžeme použít for..of
aby přes ně smyčka.
Tento příklad generuje sekvenci hodnot z start
na end
:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
for(let value of generateSequence(1, 5)) {
alert(value); // 1, then 2, then 3, then 4, then 5
}
Jak již víme, aby byl objekt iterovatelný, měli bychom přidat Symbol.iterator
k tomu.
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
return <object with next to make range iterable>
}
}
Běžná praxe pro Symbol.iterator
je vrátit generátor, zkrátí kód, jak můžete vidět:
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
for(let value of range) {
alert(value); // 1, then 2, then 3, then 4, then 5
}
Pokud chcete další podrobnosti, podívejte se prosím do kapitoly Generátory.
V běžných generátorech nemůžeme použít await
. Všechny hodnoty musí přicházet synchronně, jak vyžaduje for..of
konstrukce.
Co když bychom chtěli generovat hodnoty asynchronně? Například ze síťových požadavků.
Aby to bylo možné, přejděme na asynchronní generátory.
Asynchronní generátory (konečně)
Pro většinu praktických aplikací, kdy chceme vytvořit objekt, který asynchronně generuje posloupnost hodnot, můžeme použít asynchronní generátor.
Syntaxe je jednoduchá:předřadit function*
s async
. Díky tomu je generátor asynchronní.
A pak použijte for await (...)
iterovat to takto:
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
// Wow, can use await!
await new Promise(resolve => setTimeout(resolve, 1000));
yield i;
}
}
(async () => {
let generator = generateSequence(1, 5);
for await (let value of generator) {
alert(value); // 1, then 2, then 3, then 4, then 5 (with delay between)
}
})();
Protože je generátor asynchronní, můžeme použít await
v něm se můžete spolehnout na sliby, provádět síťové požadavky a tak dále.
Technicky vzato, pokud jste pokročilý čtenář, který si pamatuje podrobnosti o generátorech, existuje vnitřní rozdíl.
U asynchronních generátorů generator.next()
metoda je asynchronní, vrací sliby.
V běžném generátoru bychom použili result = generator.next()
získat hodnoty. V asynchronním generátoru bychom měli přidat await
, takto:
result = await generator.next(); // result = {value: ..., done: true/false}
To je důvod, proč asynchronní generátory pracují s for await...of
.
Asynchronně opakovatelný rozsah
Běžné generátory lze použít jako Symbol.iterator
aby byl iterační kód kratší.
Podobně jako asynchronní generátory lze použít jako Symbol.asyncIterator
implementovat asynchronní iteraci.
Například můžeme vytvořit range
objekt generuje hodnoty asynchronně, jednou za sekundu, nahrazením synchronního Symbol.iterator
s asynchronním Symbol.asyncIterator
:
let range = {
from: 1,
to: 5,
// this line is same as [Symbol.asyncIterator]: async function*() {
async *[Symbol.asyncIterator]() {
for(let value = this.from; value <= this.to; value++) {
// make a pause between values, wait for something
await new Promise(resolve => setTimeout(resolve, 1000));
yield value;
}
}
};
(async () => {
for await (let value of range) {
alert(value); // 1, then 2, then 3, then 4, then 5
}
})();
Nyní hodnoty přicházejí se zpožděním 1 sekundy mezi nimi.
Poznámka:
Technicky můžeme přidat obě Symbol.iterator
a Symbol.asyncIterator
k objektu, takže je obojí synchronně (for..of
) a asynchronně (for await..of
) iterovatelné.
V praxi by to však byla zvláštní věc.
Příklad ze skutečného života:stránkovaná data
Zatím jsme viděli základní příklady, abychom porozuměli. Nyní se podíváme na skutečný případ použití.
Existuje mnoho online služeb, které poskytují stránkovaná data. Když například potřebujeme seznam uživatelů, požadavek vrátí předem definovaný počet (např. 100 uživatelů) – „jedna stránka“ a poskytne adresu URL na další stránku.
Tento vzor je velmi častý. Není to o uživatelích, ale o čemkoli.
GitHub nám například umožňuje získávat commity stejným, stránkovaným způsobem:
- Měli bychom požádat o číslo
fetch
ve tvaruhttps://api.github.com/repos/<repo>/commits
. - Odpovídá JSON o 30 potvrzeních a také poskytuje odkaz na další stránku v
Link
záhlaví. - Potom můžeme tento odkaz použít pro další požadavek, získat více potvrzení a tak dále.
Pro náš kód bychom rádi měli jednodušší způsob získávání commitů.
Udělejme funkci fetchCommits(repo)
který za nás dostává commity a vytváří požadavky, kdykoli je potřeba. A ať se stará o všechny věci se stránkováním. Pro nás to bude jednoduchá asynchronní iterace for await..of
.
Takže použití bude takovéto:
for await (let commit of fetchCommits("username/repository")) {
// process commit
}
Zde je taková funkce implementovaná jako asynchronní generátor:
async function* fetchCommits(repo) {
let url = `https://api.github.com/repos/${repo}/commits`;
while (url) {
const response = await fetch(url, { // (1)
headers: {'User-Agent': 'Our script'}, // github needs any user-agent header
});
const body = await response.json(); // (2) response is JSON (array of commits)
// (3) the URL of the next page is in the headers, extract it
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
nextPage = nextPage?.[1];
url = nextPage;
for(let commit of body) { // (4) yield commits one by one, until the page ends
yield commit;
}
}
}
Další vysvětlení, jak to funguje:
-
Ke stažení odevzdání používáme metodu načítání prohlížeče.
- Počáteční adresa URL je
https://api.github.com/repos/<repo>/commits
a další stránka bude vLink
záhlaví odpovědi. fetch
metoda nám umožňuje v případě potřeby dodat autorizaci a další hlavičky – zde GitHub vyžadujeUser-Agent
.
- Počáteční adresa URL je
-
Potvrzení jsou vrácena ve formátu JSON.
-
Adresu URL další stránky bychom měli získat z
Link
záhlaví odpovědi. Má speciální formát, takže k tomu používáme regulární výraz (tuto funkci se naučíme v Regulárních výrazech).- Adresa URL další stránky může vypadat jako
https://api.github.com/repositories/93253246/commits?page=2
. Generuje jej samotný GitHub.
- Adresa URL další stránky může vypadat jako
-
Poté obdržíme přijaté odevzdání jeden po druhém, a když skončí, další
while(url)
iterace se spustí a vytvoří další požadavek.
Příklad použití (ukazuje autory odevzdání v konzoli):
(async () => {
let count = 0;
for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {
console.log(commit.author.login);
if (++count == 100) { // let's stop at 100 commits
break;
}
}
})();
// Note: If you are running this in an external sandbox, you'll need to paste here the function fetchCommits described above
Přesně to jsme chtěli.
Vnitřní mechanika stránkovaných požadavků je zvenčí neviditelná. Pro nás je to jen asynchronní generátor, který vrací potvrzení.
Shrnutí
Běžné iterátory a generátory fungují dobře s daty, jejichž generování nezabere čas.
Když očekáváme, že data budou přicházet asynchronně, se zpožděním, lze použít jejich asynchronní protějšky a for await..of
místo for..of
.
Rozdíly v syntaxi mezi asynchronními a běžnými iterátory:
Opakovatelný | Asynchronně opakovatelný | |
---|---|---|
Metoda poskytnutí iterátoru | Symbol.iterator | Symbol.asyncIterator |
next() návratová hodnota je | {value:…, done: true/false} | Promise který se převede na {value:…, done: true/false} |
Rozdíly v syntaxi mezi asynchronními a běžnými generátory:
Generátory | Asynchronní generátory | |
---|---|---|
Prohlášení | function* | async function* |
next() návratová hodnota je | {value:…, done: true/false} | Promise který se vyřeší na {value:…, done: true/false} |
Při vývoji webu se často setkáváme s proudy dat, kdy proudí kousek po kousku. Například stahování nebo nahrávání velkého souboru.
Ke zpracování takových dat můžeme použít asynchronní generátory. Je také pozoruhodné, že v některých prostředích, například v prohlížečích, existuje také další API nazvané Streams, které poskytuje speciální rozhraní pro práci s takovými streamy, pro transformaci dat a jejich předávání z jednoho streamu do druhého (např. stahování z jednoho místa a okamžitě poslat jinam).