Förstå generatorer i JavaScript

Författaren har valt Open Internet/Free Speech Fund för att få en donation som en del av programmet Write for DOnations.

Introduktion

I ECMAScript 2015 infördes generatorer i JavaScript-språket. En generator är en process som kan pausas och återupptas och som kan ge flera värden. En generator i JavaScript består av en generatorfunktion som returnerar ett iterbart Generator objekt.

Generatorer kan upprätthålla tillstånd, vilket ger ett effektivt sätt att skapa iteratorer, och kan hantera oändliga dataströmmar, vilket kan användas för att implementera oändlig rullning på frontenden av en webbapplikation, för att arbeta med ljudvågsdata, med mera. Dessutom kan generatorer, när de används med Promises, efterlikna async/await-funktionen, vilket gör att vi kan hantera asynkron kod på ett enklare och mer lättläst sätt. Även om async/await är ett mer utbrett sätt att hantera vanliga, enkla asynkrona användningsfall, som att hämta data från ett API, har generatorer mer avancerade funktioner som gör att det lönar sig att lära sig använda dem.

I den här artikeln tar vi upp hur man skapar generatorfunktioner, hur man itererar över Generator-objekt, skillnaden mellan yield och return inuti en generator och andra aspekter av att arbeta med generatorer.

Generatorfunktioner

En generatorfunktion är en funktion som returnerar ett Generator-objekt och definieras av nyckelordet function följt av en asterisk (*), enligt följande:

// Generator function declarationfunction* generatorFunction() {}

Ibland ser du asterisken bredvid funktionsnamnet, i motsats till funktionsnyckelordet, till exempel function *generatorFunction(). Detta fungerar på samma sätt, men function* är en mer allmänt accepterad syntax.

Generatorfunktioner kan också definieras i ett uttryck, som reguljära funktioner:

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

Generatorer kan till och med vara metoder för ett objekt eller en klass:

Exemplen i hela den här artikeln kommer att använda syntaxen för deklaration av generatorfunktioner.

Notera: Till skillnad från vanliga funktioner kan generatorer inte konstrueras med nyckelordet new, och de kan inte heller användas tillsammans med pilfunktioner.

Nu när du vet hur man deklarerar generatorfunktioner, ska vi titta på de iterabla Generator-objekten som de returnerar.

Generatorobjekt

Traditionellt sett körs funktioner i JavaScript till slut, och om man anropar en funktion returneras ett värde när den kommer fram till nyckelordet return. Om nyckelordet return utelämnas returnerar en funktion implicit undefined.

I följande kod deklarerar vi till exempel en sum()-funktion som returnerar ett värde som är summan av två heltalsargument:

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

Ansamlingen av funktionen returnerar ett värde som är summan av argumenten:

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

En generatorfunktion returnerar dock inte ett värde omedelbart utan returnerar i stället ett iterbart Generator-objekt. I följande exempel deklarerar vi en funktion och ger den ett enda returvärde, som en vanlig funktion:

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

När vi anropar generatorfunktionen returnerar den Generator-objektet, som vi kan tilldela en variabel:

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

Om det här var en vanlig funktion skulle vi förvänta oss att generator skulle ge oss strängen som returneras i funktionen. Men vad vi faktiskt får är ett objekt i ett suspended-tillstånd. Att anropa generator kommer därför att ge utdata som liknar följande:

Det Generator objekt som returneras av funktionen är en iterator. En iterator är ett objekt som har en next() metod tillgänglig, som används för att iterera genom en sekvens av värden. next()-metoden returnerar ett objekt med value och done egenskaper. value representerar det returnerade värdet och done anger om iteratorn har gått igenom alla sina värden eller inte.

Med detta i åtanke kan vi anropa next() på vår generator och få fram det aktuella värdet och tillståndet för iteratorn:

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

Detta ger följande utdata:

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

Värdet som returneras efter att ha anropat next() är Hello, Generator!, och tillståndet för done är true, eftersom detta värde kom från en return som stängde iteratorn. Eftersom iteratorn är avslutad ändras generatorfunktionens status från suspended till closed. Att anropa generator igen kommer att ge följande:

Output
generatorFunction {<closed>}

Från och med nu har vi bara visat hur en generatorfunktion kan vara ett mer komplext sätt att få fram return-värdet för en funktion. Men generatorfunktioner har också unika egenskaper som skiljer dem från vanliga funktioner. I nästa avsnitt lär vi oss om yield-operatören och ser hur en generator kan pausa och återuppta utförandet.

yield-operatorer

Generatorer introducerar ett nytt nyckelord i JavaScript: yield. yield kan pausa en generatorfunktion och returnera det värde som följer efter yield, vilket ger ett lättviktigt sätt att iterera genom värden.

I det här exemplet pausar vi generatorfunktionen tre gånger med olika värden och returnerar ett värde i slutet. Sedan tilldelar vi vårt Generator-objekt till generator-variabeln.

Nu, när vi anropar next() på generatorfunktionen, kommer den att pausa varje gång den stöter på yield. done kommer att sättas till false efter varje yield, vilket indikerar att generatorn inte är klar. När den möter en return, eller när det inte finns några fler yield som möts i funktionen, kommer done att slå om till true, och generatorn kommer att vara klar.

Använd metoden next() fyra gånger i rad:

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

Dessa kommer att ge följande fyra rader av utdata i tur och ordning:

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

Bemärk att en generator inte kräver ett return; om det utelämnas kommer den sista iterationen att returnera {value: undefined, done: true}, liksom alla efterföljande anrop av next() efter det att en generator är klar.

Iterera över en generator

Med hjälp av next()-metoden itererade vi manuellt genom Generator-objektet och tog emot alla value– och done-egenskaper för det fullständiga objektet. Precis som Array, Map och Set följer dock en Generator iterationsprotokollet och kan itereras igenom med for...of:

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

Detta ger följande:

Output
NeoMorpheusTrinity

Spridningsoperatorn kan också användas för att tilldela värdena i en Generator till en array.

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

Detta kommer att ge följande array:

Output
(3)

Både spread och for...of kommer inte att faktorisera return i värdena (i det här fallet skulle det ha varit 'The Oracle').

Notera: Även om båda dessa metoder är effektiva för att arbeta med ändliga generatorer, kommer det inte att vara möjligt att använda spread eller for...of direkt utan att skapa en oändlig slinga om en generator har att göra med en oändlig dataström.

Slutar en generator

Som vi har sett kan en generator få sin done-egenskap inställd på true och sin status inställd på closed genom att iterera genom alla sina värden. Det finns ytterligare två sätt att omedelbart avbryta en generator: med metoden return() och med metoden throw().

Med return() kan generatorn avslutas när som helst, precis som om ett return-uttalande hade funnits i funktionskroppen. Du kan lämna ett argument i return(), eller lämna det tomt för ett odefinierat värde.

För att demonstrera return() skapar vi en generator med några yield-värden men utan return i funktionsdefinitionen:

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

Den första next() ger oss 'Neo', med done inställt på false. Om vi åberopar en return()-metod på Generator-objektet direkt efter det får vi nu det överlämnade värdet och done är satt till true. Varje ytterligare anrop till next() kommer att ge det standardiserade avslutade generatorsvaret med ett odefinierat värde.

För att demonstrera detta kör du följande tre metoder på generator:

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

Detta kommer att ge följande tre resultat:

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

Metoden return() tvingade Generator-objektet att komplettera och ignorera alla andra yield nyckelord. Detta är särskilt användbart i asynkron programmering när du behöver göra funktioner avbrytbara, t.ex. avbryta en webbförfrågan när en användare vill utföra en annan åtgärd, eftersom det inte är möjligt att direkt avbryta en Promise.

Om kroppen av en generatorfunktion har ett sätt att fånga upp och hantera fel, kan du använda throw()-metoden för att kasta in ett fel i generatorn. Detta startar generatorn, kastar in felet och avslutar generatorn.

För att demonstrera detta kommer vi att placera en try...catch inne i generatorfunktionens kropp och logga ett fel om ett sådant hittas:

Nu kommer vi att köra next()-metoden, följt av throw():

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

Detta ger följande utdata:

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

Med hjälp av throw() injicerade vi ett fel i generatorn, vilket fångades upp av try...catch och loggades till konsolen.

Metoder och tillstånd för generatorobjekt

I följande tabell visas en lista över metoder som kan användas på Generator-objekt:

Metod Beskrivning
next() Returnerar nästa värde i en generator
return() Returnerar ett värde i en generator och avslutar generatorn
throw() Gör ett fel och avslutar generatorn

Nästa tabell listar de möjliga tillstånden för ett Generator-objekt:

Status Beskrivning
suspended Generatorn har stoppat exekveringen men har inte avslutats
closed Generatorn har avslutats genom att antingen stöta på ett fel, återgå, eller genom att iterera genom alla värden

yield Delegation

Inom den vanliga yield-operatören kan generatorer också använda yield*-uttrycket för att delegera ytterligare värden till en annan generator. När yield* påträffas i en generator kommer den att gå in i den delegerade generatorn och börja iterera genom alla yields tills den generatorn är stängd. Detta kan användas för att separera olika generatorfunktioner för att semantiskt organisera din kod, samtidigt som alla deras yields kan itereras i rätt ordning.

För att demonstrera kan vi skapa två generatorfunktioner, varav den ena yield* opererar på den andra:

Nästan, låt oss iterera genom begin() generatorfunktionen:

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

Detta ger följande värden i den ordning de genereras:

Output
1234

Den yttre generatorn gav värdena 1 och 2 och delegerade sedan till den andra generatorn med yield*, som gav 3 och 4.

yield* kan också delegera till vilket objekt som helst som är iterbart, till exempel en Array eller en Map. Yield-delegering kan vara till hjälp för att organisera kod, eftersom alla funktioner inom en generator som ville använda yield också måste vara en generator.

Oändliga dataströmmar

En av de användbara aspekterna av generatorer är förmågan att arbeta med oändliga dataströmmar och samlingar. Detta kan demonstreras genom att skapa en oändlig slinga inuti en generatorfunktion som ökar ett tal med ett.

I följande kodblock definierar vi denna generatorfunktion och startar sedan generatorn:

Nu itererar vi genom värdena med hjälp av next():

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

Detta ger följande utdata:

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

Funktionen returnerar successiva värden i den oändliga slingan samtidigt som done-egenskapen förblir false, vilket säkerställer att den inte avslutas.

Med generatorer behöver du inte oroa dig för att skapa en oändlig slinga, eftersom du kan stoppa och återuppta utförandet när du vill. Du måste dock fortfarande vara försiktig med hur du anropar generatorn. Om du använder spread eller for...of på en oändlig dataström kommer du fortfarande att iterera över en oändlig slinga på en gång, vilket kommer att få miljön att krascha.

För ett mer komplext exempel på en oändlig dataström kan vi skapa en Fibonacci-generatorfunktion. Fibonacci-sekvensen, som kontinuerligt adderar de två föregående värdena tillsammans, kan skrivas med hjälp av en oändlig slinga inom en generator enligt följande:

För att testa detta kan vi slinga oss igenom ett ändligt antal och skriva ut Fibonacci-sekvensen till konsolen.

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

Detta kommer att ge följande:

Output
0112358132134

Förmågan att arbeta med oändliga datamängder är en del av det som gör generatorer så kraftfulla. Detta kan vara användbart för exempel som att implementera oändlig rullning i frontend av ett webbprogram.

Passing Values in Generators

Under hela den här artikeln har vi använt generatorer som iteratorer, och vi har gett värden i varje iteration. Förutom att producera värden kan generatorer också konsumera värden från next(). I det här fallet kommer yield att innehålla ett värde.

Det är viktigt att notera att den första next() som anropas inte kommer att överlämna ett värde, utan endast starta generatorn. För att demonstrera detta kan vi logga värdet på yield och anropa next() några gånger med vissa värden.

Detta kommer att ge följande utdata:

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

Det är också möjligt att sätta igång generatorn med ett startvärde. I följande exempel gör vi en for-slinga och skickar varje värde till next()-metoden, men skickar också ett argument till den initiala funktionen:

Vi hämtar värdet från next() och ger ett nytt värde till nästa iteration, vilket är det föregående värdet gånger tio. Detta ger följande:

Output
010203040

Ett annat sätt att hantera uppstarten av en generator är att linda in generatorn i en funktion som alltid anropar next() en gång innan den gör något annat.

async/await med generatorer

En asynkron funktion är en typ av funktion som finns tillgänglig i ES6+ JavaScript och som gör det lättare att förstå att arbeta med asynkrona data genom att få det att framstå som synkront. Generatorer har ett mer omfattande utbud av funktioner än asynkrona funktioner, men kan replikera liknande beteenden. Genom att implementera asynkron programmering på det här sättet kan du öka flexibiliteten i din kod.

I det här avsnittet visar vi ett exempel på att reproducera async/await med hjälp av generatorer.

Låt oss bygga en asynkron funktion som använder API:et Fetch för att hämta data från API:et JSONPlaceholder (som tillhandahåller JSON-exemplariska data i testsyfte) och som loggar svaret till konsolen.

Startar du med att definiera en asynkron funktion kallad getUsers som hämtar data från API:et och returnerar en array av objekt, anropar du sedan getUsers:

Detta kommer att ge JSON-data som liknar följande:

Med hjälp av generatorer kan vi skapa något nästan identiskt som inte använder nyckelorden async/await. I stället kommer den att använda en ny funktion som vi skapar och yield-värden i stället för await-löften.

I följande kodblock definierar vi en funktion som heter getUsers som använder vår nya asyncAlt-funktion (som vi kommer att skriva senare) för att efterlikna async/await.

Som vi kan se är det nästan identiskt med async/await-implementationen, förutom att det är en generatorfunktion som skickas in och ger värden.

Nu kan vi skapa en asyncAlt-funktion som liknar en asynkron funktion. asyncAlt har en generatorfunktion som parameter, vilket är vår funktion som ger de löften som fetch returnerar. asyncAlt returnerar själv en funktion och löser upp varje löfte som den hittar fram till det sista:

Detta ger samma utdata som async/await-versionen:

Bemärk att den här implementationen är till för att demonstrera hur generatorer kan användas i stället för async/await, och att den inte är en produktionsklar design. Den har ingen felhantering inställd och har inte heller möjlighet att skicka in parametrar i de givna värdena. Även om den här metoden kan ge flexibilitet till din kod är async/await ofta ett bättre val, eftersom den abstraherar implementeringsdetaljer och låter dig fokusera på att skriva produktiv kod.

Slutsats

Generatorer är processer som kan stoppa och återuppta exekveringen. De är en kraftfull och mångsidig funktion i JavaScript, även om de inte används så ofta. I den här handledningen lärde vi oss om generatorfunktioner och generatorobjekt, metoder som är tillgängliga för generatorer, operatörerna yield och yield* samt generatorer som används med ändliga och oändliga datamängder. Vi utforskade också ett sätt att implementera asynkron kod utan inbäddade callbacks eller långa löfteskedjor.

Om du vill lära dig mer om JavaScript-syntax kan du ta en titt på våra handledningar Förstå detta, binda, anropa och tillämpa i JavaScript och Förstå Map- och Set-objekt i JavaScript.

Lämna ett svar

Din e-postadress kommer inte publiceras.