Generaattoreiden ymmärtäminen JavaScriptissä

Tekijä valitsi Open Internet/Free Speech Fund -rahaston saamaan lahjoituksen osana Write for DOnations -ohjelmaa.

Sisällysluettelo

EcMAScript 2015 -ohjelmassa JavaScript-kieleen lisättiin generaattorit. Generaattori on prosessi, joka voidaan keskeyttää ja jatkaa ja joka voi tuottaa useita arvoja. JavaScriptissä generaattori koostuu generaattorifunktiosta, joka palauttaa iteroitavan Generator objektin.

Generaattorit voivat ylläpitää tilaa, mikä tarjoaa tehokkaan tavan tehdä iteraattoreita, ja ne pystyvät käsittelemään äärettömiä tietovirtoja, joita voidaan käyttää äärettömän vierityksen toteuttamiseen web-sovelluksen frontendissä, ääniaaltodatan käsittelyyn ja muuhun. Lisäksi Promisesin kanssa käytettynä generaattorit voivat jäljitellä async/await-toiminnallisuutta, minkä ansiosta voimme käsitellä asynkronista koodia suoraviivaisemmin ja luettavammin. Vaikka async/await on yleisempi tapa käsitellä yleisiä, yksinkertaisia asynkronisia käyttötapauksia, kuten tietojen noutamista API:sta, generaattoreissa on edistyneempiä ominaisuuksia, joiden vuoksi niiden käytön opettelu kannattaa.

Tässä artikkelissa käsittelemme, miten luodaan generaattorifunktioita, miten iteroidaan Generator-objektien yli, yield:n ja return:n eroa generaattorin sisällä sekä muita generaattoreiden kanssa työskentelyyn liittyviä seikkoja.

GENERAATTORIFUNKTIOT

Generatorifunktio on funktio, joka palauttaa Generator-objektin, ja se määritellään function-avainsanalla, jota seuraa tähti (*), kuten seuraavassa näytetään:

// Generator function declarationfunction* generatorFunction() {}

Joskus näet tähden funktion nimen vieressä funktion avainsanan sijaan, esimerkiksi function *generatorFunction(). Tämä toimii samalla tavalla, mutta function* on laajemmin hyväksytty syntaksi.

Generatorfunktiot voidaan myös määritellä lausekkeessa, kuten säännölliset funktiot:

// Generator function expressionconst generatorFunction = function*() {}

Generatorfunktiot voivat olla jopa objektin tai luokan metodeja:

Tämän artikkelin esimerkeissä käytetään generaattorifunktioiden ilmoitussyntaksia.

Huomaa: Toisin kuin tavallisia funktioita, generaattoreita ei voi muodostaa new-avainsanalla, eikä niitä voi käyttää yhdessä nuolifunktioiden kanssa.

Nyt kun tiedät, miten generaattorifunktioita deklarioidaan, tarkastellaan niiden palauttamia iteroitavia Generator objekteja.

Generator-objektit

Traditionaalisesti JavaScriptissä funktiot suoritetaan loppuun asti, ja funktion kutsuminen palauttaa arvon, kun se saapuu return-avainsanaan. Jos return-avainsana jätetään pois, funktio palauttaa implisiittisesti undefined.

Esimerkiksi seuraavassa koodissa julistetaan sum()-funktio, joka palauttaa arvon, joka on kahden kokonaislukuargumentin summa:

// A regular function that sums two valuesfunction sum(a, b) { return a + b}

Funktion kutsuminen palauttaa arvon, joka on argumenttien summa:

const value = sum(5, 6) // 11

Generator-funktio ei kuitenkaan palauta arvoa heti, vaan palauttaa sen sijaan iteroitavan Generator-objektin. Seuraavassa esimerkissä julistamme funktion ja annamme sille yhden paluuarvon, kuten tavalliselle funktiolle:

// Declare a generator function with a single return valuefunction* generatorFunction() { return 'Hello, Generator!'}

Kun kutsumme generaattorifunktiota, se palauttaa Generator-olion, jonka voimme liittää muuttujaan:

// Assign the Generator object to generatorconst generator = generatorFunction()

Jos tämä olisi tavallinen funktio, odottaisimme generator:n antavan meille funktiossa palautetun merkkijonon. Todellisuudessa saamme kuitenkin suspended-tilassa olevan objektin. Kutsuminen generator antaa siis seuraavan kaltaisen tulosteen:

Funktion palauttama Generator-olio on iteraattori. Iteraattori on objekti, jolla on käytettävissä next()-metodi, jota käytetään arvosarjan iterointiin. next()-metodi palauttaa objektin, jolla on value– ja done-ominaisuudet. value edustaa palautettua arvoa, ja done kertoo, onko iteraattori käynyt läpi kaikki arvot vai ei.

Tiedostaen tämän, kutsutaan next() meidän generator:lle ja saadaan iteraattorin tämänhetkinen arvo ja tila:

// Call the next method on the Generator objectgenerator.next()

Tämä antaa seuraavan tuloksen:

Output
{value: "Hello, Generator!", done: true}

Kutsumalla next() palautettu arvo on Hello, Generator! ja done:n tila on true, koska tämä arvo tuli return:lta, joka sulki iteraattorin. Koska iteraattori on päättynyt, generaattorifunktion tila muuttuu suspended:sta closed:ksi. Kutsumalla generator uudelleen saadaan seuraava tulos:

Output
generatorFunction {<closed>}

Olemme toistaiseksi vain osoittaneet, miten generaattorifunktio voi olla monimutkaisempi tapa saada funktion return-arvo. Mutta generaattorifunktioilla on myös ainutlaatuisia ominaisuuksia, jotka erottavat ne tavallisista funktioista. Seuraavassa kappaleessa tutustumme yield-operaattoriin ja näemme, miten generaattori voi keskeyttää ja jatkaa suoritusta.

yield-operaattorit

Generattorit tuovat JavaScriptiin uuden avainsanan: yield. yield voi pysäyttää generaattorifunktion ja palauttaa yield:n jälkeisen arvon, mikä tarjoaa kevyen tavan iteroida arvoja.

Tässä esimerkissä pysäytämme generaattorifunktion kolme kertaa eri arvoilla ja palautamme lopussa arvon. Sitten osoitamme Generator-oliomme generator-muuttujaan.

Nyt kun kutsumme next() generaattorifunktiota, se pysähtyy aina kun se kohtaa yield. done muuttuu false:ksi jokaisen yield:n jälkeen, mikä osoittaa, että generaattori ei ole lopettanut. Kun se kohtaa return tai kun funktiossa ei ole enää yhtään yield:tä, done muuttuu true:ksi ja generaattori on valmis.

Käytä metodia next() neljä kertaa peräkkäin:

// Call next four timesgenerator.next()generator.next()generator.next()generator.next()

Tällöin saadaan seuraavat neljä riviä tulostetta järjestyksessä:

Output
{value: "Neo", done: false}{value: "Morpheus", done: false}{value: "Trinity", done: false}{value: "The Oracle", done: true}

Huomaa, että generaattori ei vaadi return:a; jos se jätetään pois, viimeinen iteraatio palauttaa {value: undefined, done: true}:n, samoin kuin kaikki myöhemmät kutsut metodiin next() sen jälkeen, kun generaattori on päättynyt.

Iterointi generaattorin yli

Metodin next() avulla iteroimme manuaalisesti Generator-objektin läpi ja saimme kaikki value– ja done-ominaisuudet koko objektista. Aivan kuten Array, Map ja Set, myös Generator noudattaa kuitenkin iterointiprotokollaa, ja se voidaan iteroida läpi for...of:

// Iterate over Generator objectfor (const value of generator) { console.log(value)}

Tällöin saadaan takaisin seuraavat tulokset:

Output
NeoMorpheusTrinity

Levitysoperaattoria voidaan käyttää myös Generator:n arvojen osoittamiseen arrayyn.

// Create an array from the values of a Generator objectconst values = console.log(values)

Tällöin saadaan seuraava array:

Output
(3)

Kumpikin spread ja for...of eivät kerro return:aa arvoihin (tässä tapauksessa se olisi ollut 'The Oracle').

Huomautus: Vaikka molemmat näistä menetelmistä ovat tehokkaita äärellisten generaattoreiden kanssa työskenneltäessä, jos generaattori käsittelee ääretöntä tietovirtaa, ei ole mahdollista käyttää spreadiä tai for...of suoraan ilman, että syntyy ääretön silmukka.

GENERAATTORIN SULKEMINEN

Kuten olemme nähneet, generaattorin done-ominaisuus voidaan asettaa arvoon true ja sen tila arvoon closed käymällä läpi kaikki sen arvot. On lisäksi kaksi tapaa peruuttaa generaattori välittömästi: metodilla return() ja metodilla throw().

Menetelmällä return() generaattori voidaan lopettaa missä tahansa vaiheessa, aivan kuin funktiorungossa olisi ollut return-lauseke. Voit antaa argumentin return():n sisään tai jättää sen tyhjäksi, jos kyseessä on määrittelemätön arvo.

Edistääksemme return():n, luomme generaattorin, jossa on muutama yield-arvo, mutta ei return:aa funktiomääritelmässä:

function* generatorFunction() { yield 'Neo' yield 'Morpheus' yield 'Trinity'}const generator = generatorFunction()

Ensimmäisellä next():llä saamme 'Neo':n ja done:llä asetetaan false. Jos kutsumme return()-metodia Generator-objektiin heti sen jälkeen, saamme nyt välitetyn arvon ja done:n asetettuna true:ksi. Kaikki lisäkutsut next():lle antavat oletusarvoisesti valmistuneen generaattorin vastauksen määrittelemättömällä arvolla.

Tämän havainnollistamiseksi ajetaan seuraavat kolme metodia generator:

generator.next()generator.return('There is no spoon!')generator.next()

Tällöin saadaan seuraavat kolme tulosta:

Output
{value: "Neo", done: false}{value: "There is no spoon!", done: true}{value: undefined, done: true}

Metodi return() pakotti Generator-olion suorittamaan loppuun ja jättämään huomioimatta kaikki muut yield-avainsanat. Tämä on erityisen hyödyllistä asynkronisessa ohjelmoinnissa, kun funktioista on tehtävä peruttavissa olevia, esimerkiksi keskeytettäessä web-pyyntö, kun käyttäjä haluaa suorittaa toisen toiminnon, koska Promisea ei voi suoraan peruuttaa.

Jos generaattorifunktion rungossa on keino virheiden sieppaamiseen ja käsittelyyn, voit käyttää throw()-metodia heittääksesi virheen generaattoriin. Tämä käynnistää generaattorin, heittää virheen sisään ja lopettaa generaattorin.

Tämän demonstroimiseksi laitamme try...catch generaattorifunktion rungon sisälle ja kirjaamme virheen, jos sellainen löytyy:

Jatketaan nyt metodia next() ja sen jälkeen throw():

generator.next()generator.throw(new Error('Agent Smith!'))

Tämä antaa seuraavan tulosteen:

Output
{value: "Neo", done: false}Error: Agent Smith!{value: undefined, done: true}

Käyttämällä throw() pistimme generaattoriin virheen, jonka try...catch nappasi ja kirjautui konsoliin.

Generator-objektien metodit ja tilat

Seuraavassa taulukossa on luettelo metodeista, joita voidaan käyttää Generator-objekteissa:

. generaattorissa ja lopettaa generaattorin

Metodi Kuvaus
next() Palauttaa seuraavan arvon generaattorissa
return() Palauttaa arvon
throw() Hylkää virheen ja lopettaa generaattorin

Seuraavassa taulukossa luetellaan Generator-olion mahdolliset tilat:

Tila Kuvaus
suspended Generaattori on pysäyttänyt suorituksen, mutta ei ole päättynyt
closed Generaattori on lopettanut suorituksen joko kohtaamalla virheen, palaamalla tai iteroimalla kaikki arvot läpi

yield Delegointi

Säännöllisen yield-operaattorin lisäksi generaattorit voivat käyttää yield*-lauseketta delegoidakseen lisää arvoja toiselle generaattorille. Kun yield* kohdataan generaattorin sisällä, se siirtyy delegoidun generaattorin sisälle ja alkaa iteroida kaikkia yield-lausekkeita, kunnes kyseinen generaattori suljetaan. Tätä voidaan käyttää eri generaattorifunktioiden erottamiseen koodin semanttiseksi järjestämiseksi, mutta samalla kaikki niiden yield:t ovat iteroitavissa oikeassa järjestyksessä.

Demonstraatioksi voimme luoda kaksi generaattorifunktiota, joista toinen yield* toimii toisen kanssa:

Seuraavaksi iteroidaan begin()-generaattorifunktion läpi:

// Iterate through the outer generatorconst generator = begin()for (const value of generator) { console.log(value)}

Tällöin saadaan seuraavat arvot generointijärjestyksessä:

Output
1234

Ulkoinen generaattori tuotti arvot 1 ja 2 ja delegoi sitten toiselle generaattorille yield*, joka palautti 3 ja 4.

yield* voi myös delegoida mihin tahansa iteroitavaan objektiin, kuten Arrayyn tai Mapiin. Yield-delegaatio voi olla hyödyllinen koodin organisoinnissa, sillä minkä tahansa generaattorin sisällä olevan funktion, joka haluaisi käyttää yield:aa, täytyisi olla myös generaattori.

Äärettömät tietovirrat

Yksi generaattoreiden hyödyllisistä puolista on kyky työskennellä äärettömien tietovirtojen ja kokoelmien kanssa. Tätä voidaan havainnollistaa luomalla generaattorifunktion sisälle ääretön silmukka, joka kasvattaa lukua yhdellä.

Seuraavassa koodilohkossa määrittelemme tämän generaattorifunktion ja käynnistämme sen jälkeen generaattorin:

Kerrataan nyt arvot läpi käyttämällä next():

// Iterate through the valuescounter.next()counter.next()counter.next()counter.next()

Tämä antaa seuraavan tulosteen:

Output
{value: 0, done: false}{value: 1, done: false}{value: 2, done: false}{value: 3, done: false}

Funktio palauttaa peräkkäisiä arvoja äärettömässä silmukassa, kun taas done-ominaisuus säilyy edelleen arvona false, mikä varmistaa, että se ei lopu.

Geraattoreiden avulla sinun ei tarvitse huolehtia äärettömän silmukan luomisesta, koska voit pysäyttää ja jatkaa suoritusta halutessasi. Sinun on kuitenkin edelleen oltava varovainen sen suhteen, miten kutsut generaattoria. Jos käytät leviämistä tai for...of äärettömään tietovirtaan, iteroit silti ääretöntä silmukkaa kerralla, mikä aiheuttaa ympäristön kaatumisen.

Kompleksisemman esimerkin äärettömästä tietovirrasta voimme luoda Fibonacci-generaattorifunktion. Fibonaccin sarja, joka lisää jatkuvasti kaksi edellistä arvoa yhteen, voidaan kirjoittaa käyttämällä ääretöntä silmukkaa generaattorin sisällä seuraavasti:

Kokeillaksemme tätä voimme tehdä silmukan äärellisen lukumäärän läpi ja tulostaa Fibonaccin sarjan konsoliin.

// Print the first 10 values of fibonacciconst fib = fibonacci()for (let i = 0; i < 10; i++) { console.log(fib.next().value)}

Tällöin saamme seuraavan tuloksen:

Output
0112358132134

Kyky työskennellä äärettömien datajoukkojen parissa on osa sitä, mikä tekee generaattoreista niin tehokkaita. Tämä voi olla hyödyllistä esimerkeissä, kuten äärettömän vierityksen toteuttamisessa web-sovelluksen frontendissä.

Arvojen välittäminen generaattoreissa

Kautta tämän artikkelin olemme käyttäneet generaattoreita iteraattoreina ja antaneet arvoja jokaisessa iteraatiossa. Sen lisäksi, että generaattorit tuottavat arvoja, ne voivat myös kuluttaa arvoja next(). Tässä tapauksessa yield sisältää arvon.

On tärkeää huomata, että ensimmäinen kutsuttu next() ei välitä arvoa, vaan ainoastaan käynnistää generaattorin. Tämän havainnollistamiseksi voimme kirjata yield:n arvon ja kutsua next():tä muutaman kerran joidenkin arvojen kanssa.

Tällöin saadaan seuraava tuloste:

Output
100200{value: "The end", done: true}

Generattori on myös mahdollista kylvää alkuarvolla. Seuraavassa esimerkissä teemme for-silmukan ja annamme jokaisen arvon next()-metodiin, mutta annamme argumentin myös aloitusfunktiolle:

Haemme arvon next():stä ja annamme seuraavaan iteraatioon uuden arvon, joka on edellinen arvo kertaa kymmenen. Näin saadaan seuraava:

Output
010203040

Toinen tapa käsitellä generaattorin käynnistämistä on kietoa generaattori funktioon, joka kutsuu next() aina kerran ennen kuin tekee mitään muuta.

async/await with Generators

Asynkroninen funktio (asynchronous function)

Asynkroninen funktio (asynchronous function)

on ES6+ JavaScriptissä käytettävissä oleva funktiotyyppi, joka tekee työskentelystä epäsynkronisen datan parissa entistä helpommin hahmotettavaksi tekemällä siitä näennäisesti synkronista. Generaattoreilla on laajempi valikoima ominaisuuksia kuin asynkronisilla funktioilla, mutta ne pystyvät toistamaan samanlaista käyttäytymistä. Asynkronisen ohjelmoinnin toteuttaminen tällä tavalla voi lisätä koodisi joustavuutta.

Tässä osassa esitellään esimerkki async/await:n toistamisesta generaattoreiden avulla.

Luotaan asynkroninen funktio, joka hakee Fetch-API:n avulla dataa JSONPlaceholder-API:stä (joka tarjoaa JSON-esimerkkidataa testausta varten) ja kirjaa vastauksen konsoliin.

Aloitetaan määrittelemällä asynkroninen funktio nimeltä getUsers, joka hakee dataa API:sta ja palauttaa joukko objekteja, ja kutsutaan sitten getUsers:

Tämä antaa JSON-dataa, joka on samanlaista kuin seuraavassa:

Käyttämällä generaattoreita voimme luoda jotakin melkein identtistä, joka ei käytä avainsanoja async/await. Sen sijaan se käyttää luomamme uutta funktiota ja yield-arvoja await-lupausten sijaan.

Seuraavassa koodilohkossa määrittelemme getUsers-nimisen funktion, joka käyttää uutta asyncAlt-funktiotamme (jonka kirjoitamme myöhemmin) jäljitelläkseen async/await.

Kuten näemme, se näyttää lähes identtiseltä async/await-toteutuksen kanssa, paitsi että välitetään generaattorifunktio, joka tuottaa arvoja.

Nyt voimme luoda asyncAlt-funktion, joka muistuttaa asynkronista funktiota. asyncAlt:n parametrina on generaattorifunktio, joka on meidän funktiomme, joka tuottaa lupaukset, jotka fetch palauttaa. asyncAlt palauttaa itse funktion ja ratkaisee jokaisen löytämänsä lupauksen viimeiseen lupaukseen asti:

Tämä antaa saman tuloksen kuin async/await-versio:

Huomaa, että tämä toteutus on tarkoitettu havainnollistamaan, miten generaattoreita voidaan käyttää async/await:n sijasta, eikä se ole tuotantokelpoinen rakenne. Siinä ei ole virheenkäsittelyä eikä siinä ole mahdollisuutta siirtää parametreja tuotettuihin arvoihin. Vaikka tämä menetelmä voi lisätä joustavuutta koodiin, usein async/await on parempi valinta, koska se abstrahoi toteutuksen yksityiskohdat pois ja antaa sinun keskittyä tuottavan koodin kirjoittamiseen.

Johtopäätös

Generaattorit ovat prosesseja, jotka voivat pysäyttää ja jatkaa suoritusta. Ne ovat JavaScriptin tehokas ja monipuolinen ominaisuus, vaikka niitä ei käytetäkään yleisesti. Tässä opetusohjelmassa tutustuimme generaattorifunktioihin ja generaattoriobjekteihin, generaattoreiden käytettävissä oleviin metodeihin, yield– ja yield*-operaattoreihin sekä äärellisten ja äärettömien tietojoukkojen kanssa käytettäviin generaattoreihin. Tutustuimme myös yhteen tapaan toteuttaa asynkronista koodia ilman sisäkkäisiä takaisinkutsuja tai pitkiä lupausketjuja.

Jos haluat oppia lisää JavaScriptin syntaksista, tutustu opetusohjelmiin Understanding This, Bind, Call, and Apply in JavaScript ja Understanding Map and Set Objects in JavaScript.

Vastaa

Sähköpostiosoitettasi ei julkaista.