Lyhyt esittely

Tervetuloa JavaScriptin generaattorifunktioiden ja asynkronisten generaattoreiden ihmeelliseen maailmaan.

Olet oppimassa eräästä eksoottisimmasta JavaScriptiin kuuluvasta ominaisuudesta.

Aloitetaan!

Generaattorifunktiot JavaScriptissä

Mikä on generaattorifunktio?

Generaattorifunktio (ECMAScript 2015) JavaScriptissä on erityyppinen synkroninen funktio, joka pystyy pysäyttämään ja jatkamaan suoritustaan halutessaan.

Toisin kuin tavalliset JavaScript-funktiot, jotka ovat tulta ja unohdetaan, generaattorifunktioilla on myös kyky:

  • kommunikoida kutsujan kanssa kaksisuuntaisella kanavalla.
  • säilyttää suoritusyhteytensä (scope) myöhemmissä kutsuissa.

Voit ajatella generaattorifunktioita sulkeutumisina steroideilla, mutta yhtäläisyydet loppuvat tähän!

Ensimmäinen generaattorifunktiosi

Luoaksemme generaattorifunktion laitamme tähden * function avainsanan function perään:

function* generate() {//}

Huomautus: generaattorifunktiot voivat myös ottaa luokan metodin tai funktiolausekkeen muodon. Sen sijaan nuoligeneraattorifunktiot eivät ole sallittuja.

Funktion sisällä voimme käyttää yield:tä suorituksen keskeyttämiseen:

function* generate() { yield 33; yield 99;}

yield keskeyttää suorituksen ja palauttaa niin sanotun Generator-objektin kutsujalle. Tämä objekti on samanaikaisesti sekä iteroitava, että iteraattori.

Puretaanpa nämä käsitteet.

Iteroitavat ja iteraattorit

Iteroitava objekti JavaScriptissä on objekti, joka toteuttaa Symbol.iterator. Tässä on minimaalinen esimerkki iterablesta:

const iterable = { : function() { /* TODO: iterator */ }};

Kun meillä on tämä iterable-objekti, osoitamme funktiota palauttaaksemme iteraattoriobjektin.

Tämä kuulostaa paljolta teorialta, mutta käytännössä iterable on objekti, jonka päällä voimme kiertää silmukkaa for...of:n avulla (ECMAScript 2015). Tästä lisää hetken kuluttua.

Sinun pitäisi jo tuntea pari iterablea JavaScriptissä: esimerkiksi matriisit ja merkkijonot ovat iterableja:

for (const char of "hello") { console.log(char);}

Muita tunnettuja iterableja ovat Map ja Set. for...of on myös kätevä objektin arvojen iteroinnissa:

const person = { name: "Juliana", surname: "Crain", age: 32};for (const value of Object.values(person)) { console.log(value);}

Muista vain, että kaikki ominaisuudet, jotka on merkitty enumerable: false:llä, eivät näy iteroinnissa:

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

Oman iteraattorimme ongelma on se, että se ei pääse pitkälle yksin ilman iteraattoria.

Iteraattorit ovat myös objekteja, mutta niiden pitäisi olla iteraattoriprotokollan mukaisia. Lyhyesti sanottuna iteraattoreilla on oltava vähintään next()-metodi.

next():n on palautettava toinen olio, jonka ominaisuudet ovat value ja done.

Metodimme next() logiikan on noudatettava seuraavia sääntöjä:

  • palautamme objektin done: false jatkaaksemme iteraatiota.
  • palautamme objektin done: true lopettaaksemme iteraation.

value sen sijaan pitäisi pitää sisällään tulos, jonka haluamme tuottaa kuluttajalle.

Laajennetaan esimerkkiä lisäämällä iteraattori:

const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};

Tässä meillä on iterable, joka toteuttaa oikein Symbol.iterator. Meillä on myös iteraattori, joka palauttaa:

  • olion, jonka muoto on { value: x, done: false} kunnes count saavuttaa arvon 3.
  • olion, jonka muoto on { value: x, done: true} kun count saavuttaa arvon 3.

Tämä minimaalinen iteroitava on valmis iteroitavaksi 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);}

Tulos on:

123

Kun opit tuntemaan generaattorifunktioita tarkemmin, huomaat, että iteraattorit ovat generaattoriobjektien perusta.

Pitäkää mielessä:

  • iteraattorit ovat objekteja, joita iteroimme.
  • iteraattorit ovat asioita, jotka tekevät iteraattorista … ”silmukoitavaksi” yli.

Loppujen lopuksi, mitä järkeä iteroituvassa on?

Meillä on nyt käytössämme vakiomuotoinen, luotettava for...of silmukka, joka toimii käytännöllisesti katsoen melkein mille tahansa tietorakenteelle, mukautetulle tai natiiville, JavaScriptissä.

Käyttääksesi for...of:tä mukautetussa tietorakenteessasi sinun täytyy:

  • implementoida Symbol.iterator.
  • antaa iteraattoriobjekti.

Se on siinä!

Lisälähteitä:

  • Iteratorit iteroivat (Iterators gonna iterate) kirjoittaja: Jake Archibald.

Iteraabelit ovat levitettävissä ja destrukturoitavissa

Lisäksi for...of voimme käyttää levitystä ja destrukturointia myös äärellisille iteraattoreille.

Tarkastellaan taas edellistä esimerkkiä:

const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};

Voidaksemme vetää ulos kaikki arvot voimme levittää iterablea array:

const values = ;console.log(values); // 

Voidaksemme sen sijaan vetää ulos vain pari arvoa voimme array-strukturoida iterablea. Tässä saamme iterablesta ensimmäisen ja toisen arvon:

const = iterable;console.log(first); // 1console.log(second); // 2

Tässä sen sijaan saamme ensimmäisen ja kolmannen arvon:

const = iterable;console.log(first); // 1console.log(third); // 3

Käännetään nyt katseemme jälleen generaattorifunktioihin.

Tiedon poimiminen generaattorifunktiosta

Kun generaattorifunktio on paikallaan, voimme ryhtyä vuorovaikutteiseen toimintaan sen kanssa. Tämä vuorovaikutus koostuu:

  • arvojen hakemisesta generaattorista askel askeleelta.
  • vaihtoehtoisesti arvojen lähettämisestä takaisin generaattoriin.

Noutaaksemme arvoja generaattorista voimme käyttää kolmea lähestymistapaa:

  • kutsua next() iteraattoriobjektille.
  • iterointi for...of:llä.
  • hajauttaminen ja arrayjen destrukturointi.

Generatorifunktio ei laske kaikkia tuloksiaan yhdellä kertaa, kuten tavalliset funktiot tekevät.

Jos otamme esimerkkimme, saadaksemme arvoja generaattorista, voimme ensin lämmittää generaattorin:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();

Tässä go:sta tulee iterable/iterator-objektimme, joka on kutsun generate tulos.

(Muista, että Generator-objekti on sekä iteroitava että iteraattori).

Jatkossa voimme kutsua go.next() edetäksemme suorituksessa:

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

Tässä jokainen kutsu go.next():lle tuottaa uuden objektin. Esimerkissä destrukturoimme tästä objektista ominaisuuden value.

Iteraattoriobjektin next() kutsusta next() palautetuilla objekteilla on kaksi ominaisuutta:

  • value: nykyisen askeleen arvo.
  • done: boolean-arvo, joka ilmaisee, onko generaattorissa useampia arvoja.

Toteutimme tällaiset iteraattoriobjektit edellisessä kappaleessa. Kun käytät generaattoria, iteraattoriobjekti on jo valmiiksi olemassa.

next() toimii hyvin äärellisen datan poimimiseen iteraattoriobjektista.

Epä-äärellisen datan iterointiin voimme käyttää for...of. Tässä on loputon generaattori:

function* endlessGenerator() { let counter = 0; while (true) { counter++; yield counter; }}// Consume the generatorfor (const value of endlessGenerator()) { console.log(value);}

Kuten huomaat, generaattoria ei tarvitse alustaa, kun käytetään for...of.

Loppujen lopuksi voimme myös levittää ja destrukturoida generaattoriobjektin. Tässä on levitys:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Spreadconst values = ;console.log(values); // 

Tässä on destrukturointi:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Destructuringconst = go;console.log(first); // 1console.log(second); // 2

On tärkeää huomata, että generaattorit sammuvat, kun niiden kaikki arvot on kulutettu.

Jos levität arvoja generaattorista, mitään ei ole enää jäljellä, mitä sen jälkeen vetää ulos:

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

Generaattorit toimivat myös toisinpäin: ne voivat ottaa vastaan arvoja tai komentoja kutsujalta, kuten näemme hetken kuluttua.

Palatakseni generaattorifunktioihin

Iteraattoriobjektit, generaattorifunktion kutsun tuloksena syntyvä objekti, paljastavat seuraavat metodit:

  • next()
  • return()
  • throw()

Näimme jo next():n, joka auttaa objektien vetämisessä pois generaattorista.

Sen käyttö ei rajoitu vain tietojen poimimiseen, vaan sen avulla voidaan myös lähettää arvoja generaattoriin.

Tarkastellaan seuraavaa generaattoria:

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}

Se listaa parametrin string. Annamme tämän argumentin alustuksen yhteydessä:

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");

Heti kun kutsumme next() ensimmäisen kerran iteraattoriobjektille, suoritus alkaa ja tuottaa ”A”:

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // A

Tässä vaiheessa voimme puhua takaisin generaattorille antamalla argumentin next:

go.next("b");

Tässä on täydellinen listaus:

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

Jatkossa voimme syöttää arvoja kohtaan yield aina, kun tarvitsemme uuden suuraakkosmerkkijonon:

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

Jos jossakin vaiheessa haluamme palata suorituksesta kokonaan, voimme käyttää return iteraattoriobjektiin:

const { value } = go.return("stop it");console.log(value); // stop it

Tämä pysäyttää suorituksen.

Arvojen lisäksi voit heittää generaattoriin myös poikkeuksen. Katso ”Generaattorifunktioiden virheenkäsittely”.

Generaattorifunktioiden käyttötapaukset

Useimmat kehittäjät (minä mukaan lukien) pitävät generaattorifunktioita eksoottisena JavaScript-ominaisuutena, jolla on vain vähän tai ei lainkaan sovelluksia reaalimaailmassa.

Tämä voi pitää paikkansa tavallisessa front-end-työssä, jossa ripaus jQuerya ja vähän CSS:ää riittää useimmiten.

Todellisuudessa generaattorifunktiot todella loistavat kaikissa niissä skenaarioissa, joissa suorituskyky on ensiarvoisen tärkeää.

Erityisesti ne ovat hyviä mm. seuraavissa tapauksissa:

  • työskentelyssä suurten tiedostojen ja tietokokonaisuuksien kanssa.
  • datan käsittelyssä back-endissä tai front-endissä.
  • äärettömien datasekvenssien luomisessa.
  • kalliin logiikan laskennassa pyynnöstä.

Generatorifunktiot ovat myös rakennuspalikka hienostuneille epäsynkronisille kuviotyypeille, joissa käytetään apuna epäsynkronisia generaattorifunktioita, jotka ovat aiheemme seuraavassa kappaleessa.

Asynkroniset generaattorifunktiot JavaScriptissä

Mikä on asynkroninen generaattorifunktio?

Asynkroninen generaattorifunktio (ECMAScript 2018) on erityyppinen asynkroninen funktio, joka pystyy pysäyttämään ja jatkamaan suoritustaan halutessaan.

Ero synkronisten generaattorifunktioiden ja asynkronisten generaattorifunktioiden välillä on se, että jälkimmäiset palauttavat iteraattoriobjektista asynkronisen, Promise-pohjaisen tuloksen.

Asynkroniset generaattorifunktiot pystyvät generaattorifunktioiden tavoin:

  • viestimään koollekutsujan kanssa.
  • säilyttämään suoritusyhteytensä (scope eli laajuusalueensa) myöhempien kutsujensa aikana.

Ensimmäinen asynkroninen generaattorifunktio

Luoaksemme asynkronisen generaattorifunktion julistamme generaattorifunktion tähdellä *, jonka etuliite on async:

async function* asyncGenerator() { //}

Funktion sisällä voimme käyttää funktiota yield suorituksen pysäyttämiseen:

async function* asyncGenerator() { yield 33; yield 99;}

Tässä yield pysäyttää suorituksen ja palauttaa niin sanotun Generator-olion kutsujalle.

Tämä objekti on samanaikaisesti sekä iteroitava että iteraattori.

Kerrataanpa nämä käsitteet vielä kerran, jotta nähdään, miten ne sopivat asynkroniseen maahan.

Asynkroniset iteroitavat ja iteraattorit

Javaskriptin asynkroninen iteroitava on Symbol.asyncIterator toteuttava objekti.

Tässä on minimaalinen esimerkki:

const asyncIterable = { : function() { /* TODO: iterator */ }};

Kun meillä on tämä iterable-olio, annamme funktiolle tehtäväksi palauttaa iteraattori-olion.

Iteraattoriobjektin tulee olla iteraattoriprotokollan mukainen next()-metodilla (kuten synkroninen iteraattori).

Laajennetaan esimerkkiä lisäämällä iteraattori:

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 }); } }; }};

Tämä iteraattori on samankaltainen kuin se, jonka rakensimme aiemmissa osioissa, tällä kertaa ainoana erona on se, että käärimme palauttavan objektin muotoon Promise.resolve.

Tässä vaiheessa voimme tehdä jotain tämän suuntaista:

const go = asyncIterable();go.next().then(iterator => console.log(iterator.value));go.next().then(iterator => console.log(iterator.value));// 1// 2

Vai for await...of:

async function consumer() { for await (const asyncIterableElement of asyncIterable) { console.log(asyncIterableElement); }}consumer();// 1// 2// 3

Asynkroniset iteraattorit ja iteraattorit ovat asynkronisten generaattorifunktioiden perusta.

Kohdistetaan nyt taas huomiomme niihin.

Tiedon poimiminen asynkronisesta generaattorista

Asynkroniset generaattorifunktiot eivät laske kaikkia tuloksiaan yhdessä vaiheessa, kuten tavalliset funktiot tekevät.

Sen sijaan vedämme arvot ulos askel askeleelta.

Asynkronisten iteraattoreiden ja iteraattoreiden tarkastelun jälkeen ei liene yllätys, että Promisesin vetämiseen asynkronisesta generaattorista voimme käyttää kahta lähestymistapaa:

  • kutsuminen next() iteraattoriobjektille.
  • asynkinen iterointi for await...of:llä.

Alkuperäisessä esimerkissämme voimme tehdä:

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));

Tuloste tästä koodista on:

3399

Toisessa lähestymisessä käytetään asynkistä iterointia for await...of:llä. Käyttääksemme asynkkaa iterointia käärimme kuluttajan async-funktiolla.

Tässä on täydellinen esimerkki:

async function* asyncGenerator() { yield 33; yield 99;}async function consumer() { for await (const value of asyncGenerator()) { console.log(value); }}consumer();

for await...of toimii hienosti ei-äärellisten tietovirtojen poimimiseen.

Katsotaan nyt, miten lähetämme datan takaisin generaattorille.

Palataan takaisin asynkronisiin generaattorifunktioihin

Harkitaan seuraavaa asynkronista generaattoria:

async function* asyncGenerator(string) { while (true) { string = yield string.toUpperCase(); }}

Samoin kuin generaattoriesimerkin loputtomalle suurkoneelle, voimme antaa argumentin next():

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();

Näissä jokaisella askeleella generaattoriin lähetetään uusi arvo.

Tämän koodin tulos on:

ABC

Vaikka toUpperCase():ssa ei ole mitään luonnostaan asynkronista, voit aistia tämän kuvion tarkoituksen.

Jos jossain vaiheessa haluamme poistua suorituksesta, voimme kutsua return() iteraattoriobjektiin:

 const { value } = await go.return("stop it"); console.log(value); // stop it

Arvojen lisäksi voit myös heittää poikkeuksen generaattoriin. Katso ”Virheenkäsittely asynkronisissa generaattoreissa”.

Asynkronisten iteraattoreiden ja asynkronisten generaattorifunktioiden käyttötapaukset

Jos generaattorifunktiot sopivat hyvin synkroniseen työskentelyyn suurten tiedostojen ja äärettömien sekvenssien kanssa, asynkroniset generaattorifunktiot mahdollistavat JavaScriptille aivan uudenlaisen mahdollisuuksien maan.

Erityisesti asynkroninen iterointi helpottaa luettavien virtojen kuluttamista. Fetchin Response-objekti paljastaa body luettavaksi virraksi getReader(). Voimme kietoa tällaisen virran asynkronisella generaattorilla ja myöhemmin iteroida sitä for await...of:llä.

Jake Archibaldin teoksessa Async iterators and generators on joukko hienoja esimerkkejä.

Muita esimerkkejä streameista ovat request streams with Fetch.

Kirjoitushetkellä ei ole selaimen API:ta, joka toteuttaisi Symbol.asyncIterator, mutta Stream spec tulee muuttamaan tämän.

In Node.js viimeaikainen Stream API pelaa hienosti asynkronisten generaattoreiden ja asynkronisen iteroinnin kanssa.

Tulevaisuudessa pystymme kuluttamaan ja työskentelemään saumattomasti kirjoitettavien, luettavien ja muuntuvien virtojen kanssa asiakaspuolella.

Lisälähteitä:

  • The Stream API.

Wrapping up

Keskeiset termit ja käsitteet, joita käsiteltiin tässä postauksessa:

ECMAScript 2015:

  • iterable.
  • iteraattori.

Nämä ovat generaattorifunktioiden rakennuspalikoita.

ECMAScript 2018:

  • asynkroninen iterable.
  • asynkroninen iteraattori.

Nämä ovat sen sijaan asynkronisten generaattorifunktioiden rakennuspalikoita.

Hyvä ymmärrys iteraattoreista ja iteraattoreista voi viedä sinut pitkälle. Ei ole niin, että työskentelet generaattorifunktioiden ja asynkronisten generaattorifunktioiden kanssa joka päivä, mutta ne ovat mukava taito työkaluvyölläsi.

Ja sinä? Oletko koskaan käyttänyt niitä?

Kiitos lukemisesta ja pysy kuulolla tässä blogissa!

Vastaa

Sähköpostiosoitettasi ei julkaista.