Forståelse af generatorer i JavaScript

Forfatteren har valgt Open Internet/Free Speech Fund til at modtage en donation som en del af Write for DOnations-programmet.

Introduktion

I ECMAScript 2015 blev generatorer indført i JavaScript-sproget. En generator er en proces, der kan sættes på pause og genoptages, og som kan give flere værdier. En generator i JavaScript består af en generatorfunktion, som returnerer et iterbart Generator objekt.

Generatorer kan opretholde tilstand, hvilket giver en effektiv måde at lave iteratorer på, og de er i stand til at håndtere uendelige datastrømme, hvilket kan bruges til at implementere uendelig rulning på frontend af en webapplikation, til at operere med lydbølgedata og meget mere. Desuden kan generatorer, når de bruges sammen med Promises, efterligne async/await-funktionaliteten, hvilket giver os mulighed for at håndtere asynkron kode på en mere ligefrem og læsbar måde. Selv om async/await er en mere udbredt måde at håndtere almindelige, enkle asynkrone anvendelsestilfælde, som f.eks. at hente data fra et API, har generatorer mere avancerede funktioner, der gør det værd at lære at bruge dem.

I denne artikel vil vi dække, hvordan man opretter generatorfunktioner, hvordan man itererer over Generator-objekter, forskellen mellem yield og return inde i en generator og andre aspekter af arbejdet med generatorer.

Generatorfunktioner

En generatorfunktion er en funktion, der returnerer et Generator-objekt, og som er defineret ved nøgleordet function efterfulgt af en asterisk (*), som vist i det følgende:

// Generator function declarationfunction* generatorFunction() {}

Fiktivt vil du se asterisken ved siden af funktionsnavnet i modsætning til funktionsnøgleordet, f.eks. function *generatorFunction(). Dette fungerer på samme måde, men function* er en mere almindeligt accepteret syntaks.

Generatorfunktioner kan også defineres i et udtryk, ligesom regulære funktioner:

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

Generatorer kan endda være metoderne i et objekt eller en klasse:

Eksemplerne i hele denne artikel vil bruge generatorfunktionsdeklarationssyntaksen.

Bemærk: I modsætning til almindelige funktioner kan generatorer ikke konstrueres med nøgleordet new, og de kan heller ikke bruges sammen med pilefunktioner.

Nu da du ved, hvordan man erklærer generatorfunktioner, skal vi se på de iterable Generator-objekter, som de returnerer.

Generatorobjekter

Traditionelt kører funktioner i JavaScript til afslutning, og ved at kalde en funktion returneres der en værdi, når den når frem til nøgleordet return. Hvis nøgleordet return udelades, returnerer en funktion implicit undefined.

I følgende kode erklærer vi f.eks. en sum()-funktion, der returnerer en værdi, som er summen af to heltalsargumenter:

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

Kaldes funktionen, returneres en værdi, som er summen af argumenterne:

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

En generatorfunktion returnerer dog ikke en værdi med det samme, men returnerer i stedet et iterbart Generator-objekt. I følgende eksempel erklærer vi en funktion og giver den en enkelt returværdi, ligesom en standardfunktion:

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

Når vi kalder generatorfunktionen, returnerer den Generator-objektet, som vi kan tildele en variabel:

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

Hvis dette var en almindelig funktion, ville vi forvente, at generator ville give os den streng, der returneres i funktionen. Men det, vi faktisk får, er et objekt i en suspended-tilstand. Hvis vi kalder generator, vil vi derfor få et output svarende til følgende:

Det Generator objekt, der returneres af funktionen, er en iterator. En iterator er et objekt, der har en next() metode til rådighed, som bruges til at iterere gennem en sekvens af værdier. next()-metoden returnerer et objekt med value og done-egenskaber. value repræsenterer den returnerede værdi, og done angiver, om iteratoren har kørt alle værdierne igennem eller ej.

Med denne viden kan vi kalde next() på vores generator og få den aktuelle værdi og status for iteratoren:

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

Dette vil give følgende output:

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

Værdien, der returneres ved at kalde next(), er Hello, Generator!, og status for done er true, fordi denne værdi kom fra en return, der lukkede iteratoren. Da iteratoren er afsluttet, ændres generatorfunktionens status fra suspended til closed. Hvis man kalder generator igen, vil det give følgende:

Output
generatorFunction {<closed>}

Så vidt jeg kan se, har vi kun demonstreret, hvordan en generatorfunktion kan være en mere kompleks måde at få return-værdien af en funktion på. Men generatorfunktioner har også unikke egenskaber, der adskiller dem fra normale funktioner. I næste afsnit lærer vi om yield-operatoren og ser, hvordan en generator kan holde pause og genoptage udførelsen.

yield-operatorer

Generatorer introducerer et nyt nøgleord i JavaScript: yield. yield kan sætte en generatorfunktion på pause og returnere den værdi, der følger efter yield, hvilket giver en letvægtsmetode til at iterere gennem værdier.

I dette eksempel sætter vi generatorfunktionen på pause tre gange med forskellige værdier og returnerer en værdi til sidst. Derefter tildeler vi vores Generator-objekt til generator-variablen.

Nu, når vi kalder next() på generatorfunktionen, vil den holde pause, hver gang den støder på yield. done vil blive sat til false efter hver yield, hvilket angiver, at generatoren ikke er færdig. Når den støder på et return, eller når der ikke støder på flere yield i funktionen, vil done slå om til true, og generatoren vil være færdig.

Brug metoden next() fire gange i træk:

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

Disse vil give følgende fire linjer output i rækkefølge:

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

Bemærk, at en generator ikke kræver en return; hvis den udelades, vil den sidste iteration returnere {value: undefined, done: true}, ligesom alle efterfølgende kald til next(), efter at en generator er afsluttet.

Iteration over en generator

Ved hjælp af next()-metoden itererede vi manuelt gennem Generator-objektet og modtog alle value– og done-egenskaberne for det fulde objekt. Ligesom Array, Map og Set følger en Generator imidlertid iterationsprotokollen og kan itereres igennem med for...of:

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

Dette vil returnere følgende:

Output
NeoMorpheusTrinity

Spread-operatoren kan også bruges til at tildele værdierne i en Generator til et array.

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

Dette vil give følgende array:

Output
(3)

Både spredningen og for...of vil ikke faktorisere return i værdierne (i dette tilfælde ville det have været 'The Oracle').

Bemærk: Mens begge disse metoder er effektive til at arbejde med begrænsede generatorer, vil det ikke være muligt at bruge spread eller for...of direkte uden at skabe en uendelig sløjfe, hvis en generator har at gøre med en uendelig datastrøm.

Slutter en generator

Som vi har set, kan en generator få sin done-egenskab sat til true og sin status sat til closed ved at iterere gennem alle dens værdier. Der er yderligere to måder at afbryde en generator med det samme: med return()-metoden og med throw()-metoden.

Med return() kan generatoren afbrydes på et hvilket som helst tidspunkt, ligesom hvis der havde været en return-anvisning i funktionens krop. Du kan overgive et argument i return() eller lade det være tomt for en udefineret værdi.

For at demonstrere return() opretter vi en generator med et par yield-værdier, men uden return i funktionsdefinitionen:

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

Den første next() giver os 'Neo', med done sat til false. Hvis vi påkalder en return()-metode på Generator-objektet lige efter det, får vi nu den overgivne værdi og done sat til true. Ethvert yderligere kald til next() vil give det standardiserede færdige generatorsvar med en udefineret værdi.

For at demonstrere dette skal du køre følgende tre metoder på generator:

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

Det vil give følgende tre resultater:

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

Metoden return() tvang Generator-objektet til at blive færdigt og til at ignorere alle andre yield-keywords. Dette er især nyttigt i asynkron programmering, når du skal gøre funktioner annullerbare, f.eks. ved at afbryde en webanmodning, når en bruger ønsker at udføre en anden handling, da det ikke er muligt at annullere en Promise direkte.

Hvis kroppen af en generatorfunktion har en måde at fange og håndtere fejl på, kan du bruge throw()-metoden til at kaste en fejl ind i generatoren. Dette starter generatoren, kaster fejlen ind og afslutter generatoren.

For at demonstrere dette vil vi sætte en try...catch ind i generatorfunktionens krop og logge en fejl, hvis der findes en:

Nu vil vi køre next()-metoden efterfulgt af throw():

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

Dette vil give følgende output:

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

Ved hjælp af throw() injicerede vi en fejl i generatoren, som blev fanget af try...catch og logget til konsollen.

Generatorobjektmetoder og -tilstande

Den følgende tabel viser en liste over metoder, der kan bruges på Generator-objekter:

Metode Beskrivelse
next() Returnerer den næste værdi i en generator
return() Returnerer en værdi i en generator og afslutter generatoren
throw() Skalder en fejl og afslutter generatoren

Den næste tabel viser de mulige tilstande for et Generator-objekt:

Status Beskrivelse
suspended Generatoren har standset udførelsen, men er ikke afsluttet
closed Generatoren er afsluttet ved enten at være stødt på en fejl, vender tilbage, eller gentager gennem alle værdier

yield Delegation

Ud over den almindelige yield-operator kan generatorer også bruge yield*-udtrykket til at delegere yderligere værdier til en anden generator. Når yield* påtræffes i en generator, går den ind i den delegerede generator og begynder at iterere gennem alle yields, indtil denne generator er lukket. Dette kan bruges til at adskille forskellige generatorfunktioner for at organisere din kode semantisk, samtidig med at alle deres yields stadig kan itereres i den rigtige rækkefølge.

For at demonstrere kan vi oprette to generatorfunktioner, hvoraf den ene vil yield* operere på den anden:

Næst skal vi iterere gennem begin()-generatorfunktionen:

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

Dette vil give følgende værdier i den rækkefølge, de er genereret:

Output
1234

Den ydre generator gav værdierne 1 og 2, hvorefter den delegerede til den anden generator med yield*, som gav 3 og 4.

yield* kan også delegere til et hvilket som helst objekt, der er iterbart, f.eks. et Array eller et Map. Yield-delegering kan være nyttig i forbindelse med organisering af kode, da enhver funktion i en generator, der ønskede at bruge yield, også skulle være en generator.

Uendelige datastrømme

Et af de nyttige aspekter ved generatorer er muligheden for at arbejde med uendelige datastrømme og samlinger. Dette kan demonstreres ved at oprette en uendelig løkke inde i en generatorfunktion, der øger et tal med én.

I følgende kodeblok definerer vi denne generatorfunktion og starter derefter generatoren:

Itererér nu gennem værdierne ved hjælp af next():

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

Dette vil give følgende output:

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

Funktionen returnerer successive værdier i den uendelige sløjfe, mens done-egenskaben forbliver false, hvilket sikrer, at den ikke afsluttes.

Med generatorer behøver du ikke at bekymre dig om at skabe en uendelig løkke, fordi du kan standse og genoptage udførelsen efter behag. Du skal dog stadig være forsigtig med, hvordan du påkalder generatoren. Hvis du bruger spread eller for...of på en uendelig datastrøm, vil du stadig iterere over en uendelig løkke på én gang, hvilket vil få miljøet til at gå ned.

For et mere komplekst eksempel på en uendelig datastrøm, kan vi oprette en Fibonacci-generatorfunktion. Fibonacci-sekvensen, som kontinuerligt lægger de to foregående værdier sammen, kan skrives ved hjælp af en uendelig løkke i en generator på følgende måde:

For at afprøve dette kan vi gennemløbe en løkke gennem et endeligt tal og udskrive Fibonacci-sekvensen til konsollen.

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

Dette vil give følgende:

Output
0112358132134

Muligheden for at arbejde med uendelige datasæt er en del af det, der gør generatorer så kraftfulde. Dette kan være nyttigt i eksempler som f.eks. implementering af uendelig rulning på frontend af et webprogram.

Givelse af værdier i generatorer

Igennem hele denne artikel har vi brugt generatorer som iteratorer, og vi har givet værdier i hver iteration. Ud over at producere værdier kan generatorer også forbruge værdier fra next(). I dette tilfælde vil yield indeholde en værdi.

Det er vigtigt at bemærke, at den første next(), der kaldes, ikke vil overdrage en værdi, men kun starte generatoren. For at demonstrere dette kan vi logge værdien af yield og kalde next() et par gange med nogle værdier.

Dette vil give følgende output:

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

Det er også muligt at seed generatoren med en startværdi. I det følgende eksempel laver vi en for-loop og sender hver værdi ind i next()-metoden, men sender også et argument til den indledende funktion:

Vi henter værdien fra next() og afgiver en ny værdi til den næste iteration, som er den foregående værdi gange ti. Dette vil give følgende:

Output
010203040

En anden måde at håndtere opstart af en generator på er at indpakke generatoren i en funktion, der altid kalder next() én gang, før der gøres noget andet.

async/await med generatorer

En asynkron funktion er en type funktion, der er tilgængelig i ES6+ JavaScript, og som gør det lettere at forstå arbejdet med asynkrone data ved at få det til at fremstå synkront. Generatorer har en mere omfattende vifte af muligheder end asynkrone funktioner, men er i stand til at replikere lignende adfærd. Ved at implementere asynkron programmering på denne måde kan du øge fleksibiliteten i din kode.

I dette afsnit demonstrerer vi et eksempel på reproduktion af async/await med generatorer.

Lad os opbygge en asynkron funktion, der bruger Fetch API’et til at hente data fra JSONPlaceholder API’et (som leverer JSON-eksempeldata til testformål) og logger svaret til konsollen.

Start med at definere en asynkron funktion kaldet getUsers, der henter data fra API’et og returnerer et array af objekter, og kald derefter getUsers:

Dette vil give JSON-data svarende til følgende:

Ved hjælp af generatorer kan vi oprette noget næsten identisk, der ikke bruger nøgleordene async/await. I stedet vil det bruge en ny funktion, som vi opretter, og yield-værdier i stedet for await-løfter.

I den følgende kodeblok definerer vi en funktion kaldet getUsers, der bruger vores nye asyncAlt-funktion (som vi skriver senere) til at efterligne async/await.

Som vi kan se, ser det næsten identisk ud med async/await-implementeringen, bortset fra at der overføres en generatorfunktion, som giver værdier.

Nu kan vi oprette en asyncAlt-funktion, der ligner en asynkron funktion. asyncAlt har en generatorfunktion som en parameter, som er vores funktion, der giver de løfter, som fetch returnerer. asyncAlt returnerer selv en funktion og løser hvert løfte, den finder, indtil det sidste:

Dette vil give det samme output som async/await-versionen:

Bemærk, at denne implementering er til demonstration af, hvordan generatorer kan bruges i stedet for async/await, og er ikke et produktionsklart design. Den har ikke fejlhåndtering opsat, og den har heller ikke mulighed for at overføre parametre i de afgivne værdier. Selv om denne metode kan tilføje fleksibilitet til din kode, vil async/await ofte være et bedre valg, da den abstraherer implementeringsdetaljerne og lader dig fokusere på at skrive produktiv kode.

Slutning

Generatorer er processer, der kan standse og genoptage udførelsen. De er en kraftfuld og alsidig funktion i JavaScript, selv om de ikke bruges så ofte. I denne vejledning lærte vi om generatorfunktioner og generatorobjekter, metoder, der er tilgængelige for generatorer, operatørerne yield og yield* og generatorer, der anvendes med begrænsede og uendelige datasæt. Vi undersøgte også en måde at implementere asynkron kode på uden indlejrede callbacks eller lange løftekæder.

Hvis du gerne vil lære mere om JavaScript-syntaks, kan du tage et kig på vores tutorials Forstå dette, bind, kald og anvend i JavaScript og Forstå Map- og sætobjekter i JavaScript.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.