Asynchronní iterace a generátory

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 s next 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>} , kde done: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ě:

  1. Použijte Symbol.asyncIterator místo Symbol.iterator .
  2. 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řit async next() .
  3. Pro iteraci takového objektu bychom měli použít for await (let item of iterable) smyčka.
    • Všimněte si await slovo.

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:

  1. Aby objekt mohl být asynchronně iterovatelný, musí mít metodu Symbol.asyncIterator (1) .
  2. Tato metoda musí vrátit objekt s next() metoda vracející slib (2) .
  3. next() metoda nemusí být async , může to být běžná metoda vracející slib, ale async nám umožňuje používat await , takže je to pohodlné. Zde jen zdržíme na sekundu (3) .
  4. 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é jeho next() 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
Syntaxe šíření ... 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.

Rozdíl pod kapotou

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 tvaru https://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:

  1. 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 v Link záhlaví odpovědi.
    • fetch metoda nám umožňuje v případě potřeby dodat autorizaci a další hlavičky – zde GitHub vyžaduje User-Agent .
  2. Potvrzení jsou vrácena ve formátu JSON.

  3. 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.
  4. 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).