Generatoren in JavaScript begrijpen

De auteur heeft het Open Internet/Free Speech Fund geselecteerd om een donatie te ontvangen als onderdeel van het Write for DOnations-programma.

Inleiding

In ECMAScript 2015 zijn generatoren geïntroduceerd in de JavaScript-taal. Een generator is een proces dat gepauzeerd en hervat kan worden en meerdere waarden kan opleveren. Een generator in JavaScript bestaat uit een generatorfunctie, die een iterabel Generator object retourneert.

Generators kunnen de toestand behouden, waardoor een efficiënte manier ontstaat om iterators te maken, en kunnen omgaan met oneindige gegevensstromen, die kunnen worden gebruikt om oneindig scrollen op de voorkant van een webapplicatie te implementeren, om geluidsgolfgegevens te bewerken, en nog veel meer. Bovendien kunnen generatoren, wanneer ze gebruikt worden met Promises, de async/await functionaliteit nabootsen, wat ons in staat stelt om asynchrone code op een meer eenvoudige en leesbare manier te behandelen. Hoewel async/await een meer voorkomende manier is om om te gaan met gemeenschappelijke, eenvoudige asynchrone use cases, zoals het ophalen van gegevens uit een API, hebben generatoren meer geavanceerde functies die het leren hoe ze te gebruiken de moeite waard maken.

In dit artikel zullen we behandelen hoe je generator functies maakt, hoe je iterate over Generator objecten, het verschil tussen yield en return binnen een generator, en andere aspecten van het werken met generatoren.

Generator Functies

Een generator functie is een functie die een Generator object retourneert, en wordt gedefinieerd door het function sleutelwoord gevolgd door een sterretje (*), zoals in het volgende:

// Generator function declarationfunction* generatorFunction() {}

Soms zult u het sterretje naast de functienaam zien, in tegenstelling tot het functie sleutelwoord, zoals function *generatorFunction(). Dit werkt hetzelfde, maar function* is een meer algemeen geaccepteerde syntaxis.

Generatorfuncties kunnen ook worden gedefinieerd in een expressie, zoals reguliere functies:

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

Generatoren kunnen zelfs de methoden van een object of klasse zijn:

De voorbeelden in dit artikel zullen de declaratiesyntaxis van generatorfuncties gebruiken.

Note: in tegenstelling tot gewone functies kunnen generatoren niet worden geconstrueerd met het new-sleutelwoord, noch kunnen ze worden gebruikt in combinatie met pijlfuncties.

Nu u weet hoe u generatorfuncties kunt declareren, laten we eens kijken naar de iterabele Generator objecten die ze retourneren.

Generator-objecten

Traditioneel gezien worden functies in JavaScript voltooid, en wanneer een functie wordt aangeroepen, wordt een waarde geretourneerd wanneer deze bij het return-sleutelwoord is aangekomen. Als het return sleutelwoord wordt weggelaten, zal een functie impliciet undefined teruggeven.

In de volgende code, bijvoorbeeld, geven we een sum() functie aan die een waarde retourneert die de som is van twee gehele argumenten:

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

Het aanroepen van de functie retourneert een waarde die de som is van de argumenten:

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

Een generator functie retourneert echter niet onmiddellijk een waarde, en retourneert in plaats daarvan een iterable Generator object. In het volgende voorbeeld declareren we een functie en geven deze een enkele retourwaarde, zoals een standaardfunctie:

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

Wanneer we de generator-functie aanroepen, retourneert deze het object Generator, dat we kunnen toewijzen aan een variabele:

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

Als dit een gewone functie was, zouden we verwachten dat generator ons de tekenreeks zou geven die in de functie wordt geretourneerd. Maar wat we in werkelijkheid krijgen is een object in een suspended toestand. Het aanroepen van generator zal daarom uitvoer geven die lijkt op het volgende:

Het Generator object dat door de functie wordt geretourneerd is een iterator. Een iterator is een object dat een next()-methode beschikbaar heeft, die wordt gebruikt voor het itereren door een reeks waarden. De methode next() retourneert een object met value en done eigenschappen. value staat voor de geretourneerde waarde, en done geeft aan of de iterator al zijn waarden heeft doorlopen of niet.

Dit wetende, laten we next() oproepen op onze generator en de huidige waarde en toestand van de iterator opvragen:

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

Dit zal de volgende uitvoer geven:

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

De waarde die terugkomt van het oproepen van next() is Hello, Generator!, en de toestand van done is true, omdat deze waarde kwam van een return die de iterator afsloot. Omdat de iterator klaar is, zal de status van de generator-functie veranderen van suspended in closed. Opnieuw aanroepen van generator geeft het volgende:

Output
generatorFunction {<closed>}

Vooralsnog hebben we alleen laten zien hoe een generator functie een meer complexe manier kan zijn om de return waarde van een functie te krijgen. Maar generator functies hebben ook unieke eigenschappen die ze onderscheiden van normale functies. In de volgende sectie zullen we leren over de yield operator en zien hoe een generator de uitvoering kan pauzeren en hervatten.

yield Operators

Generators introduceren een nieuw sleutelwoord in JavaScript: yield. yield kan een generator functie pauzeren en de waarde teruggeven die volgt op yield, waardoor een lichtgewicht manier ontstaat om door waarden te itereren.

In dit voorbeeld zullen we de generator functie drie keer pauzeren met verschillende waarden, en aan het eind een waarde teruggeven. Daarna wijzen we ons object Generator toe aan de variabele generator.

Nu, als we next() oproepen op de generator-functie, zal deze pauzeren telkens als hij yield tegenkomt. done zal op false worden gezet na elke yield, om aan te geven dat de generator nog niet klaar is. Zodra een return wordt gevonden, of er geen yields meer worden gevonden in de functie, zal done op true worden gezet, en de generator zal zijn voltooid.

Gebruik de methode next() vier keer achter elkaar:

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

Deze geven de volgende vier regels uitvoer in volgorde:

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

Merk op dat een generator geen return nodig heeft; indien weggelaten, zal de laatste iteratie {value: undefined, done: true} teruggeven, net als alle volgende aanroepen van next() nadat een generator is voltooid.

Iterating Over a Generator

Met behulp van de methode next() hebben we handmatig het Generator object ge-enterateerd, waarbij we alle value en done eigenschappen van het volledige object hebben ontvangen. Echter, net als Array, Map, en Set, volgt een Generator het iteratie protocol, en kan worden doorlopen met for...of:

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

Dit zal het volgende opleveren:

Output
NeoMorpheusTrinity

De spread operator kan ook worden gebruikt om de waarden van een Generator aan een array toe te wijzen.

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

Dit levert de volgende matrix op:

Output
(3)

Zowel spread als for...of zullen de return niet in de waarden verdisconteren (in dit geval zou dat 'The Oracle' zijn geweest).

Note: Hoewel beide methoden effectief zijn voor het werken met eindige generators, als een generator te maken heeft met een oneindige gegevensstroom, zal het niet mogelijk zijn om spread of for...of direct te gebruiken zonder een oneindige lus te creëren.

Een Generator afsluiten

Zoals we hebben gezien, kan een generator zijn done eigenschap op true laten zetten en zijn status op closed door al zijn waarden te itereren. Er zijn twee extra manieren om een generator onmiddellijk te annuleren: met de methode return(), en met de methode throw().

Met return(), kan de generator op elk punt worden beëindigd, net alsof een return statement in de body van de functie had gestaan. Je kunt een argument doorgeven aan return(), of het leeg laten voor een ongedefinieerde waarde.

Om return() te demonstreren, zullen we een generator maken met een paar yield waarden, maar geen return in de functie-definitie:

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

De eerste next() geeft ons 'Neo', met done ingesteld op false. Als we direct daarna een return() methode op het Generator object aanroepen, krijgen we nu de doorgegeven waarde en done ingesteld op true. Elke verdere aanroep van next() zal de standaard voltooide generator respons geven met een ongedefinieerde waarde.

Om dit te demonstreren, voer de volgende drie methoden uit op generator:

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

Dit zal de volgende drie resultaten geven:

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

De return() methode dwong het Generator object om te voltooien en om alle andere yield sleutelwoorden te negeren. Dit is vooral handig in asynchroon programmeren wanneer u functies annuleerbaar moet maken, zoals het onderbreken van een web request wanneer een gebruiker een andere actie wil uitvoeren, omdat het niet mogelijk is om een Promise direct te annuleren.

Als de body van een generator functie een manier heeft om fouten op te vangen en te behandelen, kunt u de throw() methode gebruiken om een fout in de generator te gooien. Dit start de generator op, gooit de fout erin, en beëindigt de generator.

Om dit te demonstreren, zullen we een try...catch in de body van de generator functie zetten en een fout loggen als er een wordt gevonden:

Nu zullen we de next() methode uitvoeren, gevolgd door throw():

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

Dit geeft de volgende uitvoer:

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

Met throw() hebben we een fout in de generator geïnjecteerd, die werd opgevangen door try...catch en gelogd naar de console.

Generator Object Methods and States

De volgende tabel toont een lijst van methodes die kunnen worden gebruikt op Generator objecten:

Methode Omschrijving
next() Retourneert de volgende waarde in een generator
return() Retourneert een waarde in een generator en beëindigt de generator
throw() Gooit een fout en beëindigt de generator

De volgende tabel geeft een overzicht van de mogelijke toestanden van een Generator object:

Status Beschrijving
suspended Generator is gestopt met de uitvoering, maar is niet beëindigd
closed Generator is beëindigd door ofwel op een fout te stuiten, terug te keren, of alle waarden te doorlopen

yield Delegatie

Naast de gewone yield operator kunnen generators ook de yield* expressie gebruiken om verdere waarden aan een andere generator te delegeren. Wanneer de yield* wordt aangetroffen in een generator, zal deze binnen de gedelegeerde generator gaan en beginnen met het doorlopen van alle yields totdat die generator is gesloten. Dit kan worden gebruikt om verschillende generator functies te scheiden om semantisch je code te organiseren, terwijl nog steeds al hun yields in de juiste volgorde itereerbaar zijn.

Om dit te demonstreren, kunnen we twee generator functies maken, waarvan de ene yield* op de andere zal werken:

Na, laten we de begin() generator functie itereren:

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

Dit zal de volgende waarden opleveren in de volgorde waarin ze zijn gegenereerd:

Output
1234

De buitenste generator leverde de waarden 1 en 2 op, en delegeerde vervolgens naar de andere generator met yield*, die 3 en 4 opleverde.

yield* kan ook delegeren naar elk object dat iterable is, zoals een Array of een Map. Opbrengstdelegatie kan handig zijn bij het organiseren van code, omdat elke functie binnen een generator die yield zou willen gebruiken ook een generator zou moeten zijn.

oneindige gegevensstromen

Een van de nuttige aspecten van generatoren is de mogelijkheid om te werken met oneindige gegevensstromen en verzamelingen. Dit kan worden gedemonstreerd door een oneindige lus te maken in een generator-functie die een getal met één verhoogt.

In het volgende codeblok definiëren we deze generatorfunctie en starten we de generator:

Doorloop nu de waarden met next():

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

Dit levert de volgende uitvoer op:

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

De functie retourneert opeenvolgende waarden in de oneindige lus terwijl de done eigenschap false blijft, zodat hij niet zal eindigen.

Met generatoren hoeft u zich geen zorgen te maken over het ontstaan van een oneindige lus, omdat u de uitvoering naar believen kunt stoppen en hervatten. Je moet echter nog steeds voorzichtig zijn met hoe je de generator aanroept. Als je spread of for...of gebruikt op een oneindige gegevensstroom, zul je nog steeds in één keer over een oneindige lus itereren, waardoor de omgeving zal crashen.

Voor een complexer voorbeeld van een oneindige gegevensstroom, kunnen we een Fibonacci generator functie maken. De Fibonacci rij, die continu de twee vorige waarden bij elkaar optelt, kan worden geschreven met behulp van een oneindige lus binnen een generator als volgt:

Om dit uit te testen, kunnen we door een eindig getal lopen en de Fibonacci rij op de console afdrukken.

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

Dit geeft het volgende:

Output
0112358132134

De mogelijkheid om met oneindige gegevensverzamelingen te werken is een deel van wat generatoren zo krachtig maakt. Dit kan nuttig zijn voor voorbeelden zoals het implementeren van oneindig scrollen aan de voorkant van een webapplicatie.

Waarden doorgeven in generatoren

Doorheen dit artikel hebben we generatoren gebruikt als iteratoren, en we hebben waarden opgeleverd in elke iteratie. Naast het produceren van waarden, kunnen generatoren ook waarden consumeren van next(). In dit geval zal yield een waarde bevatten.

Het is belangrijk op te merken dat de eerste next() die wordt aangeroepen geen waarde zal doorgeven, maar alleen de generator zal starten. Om dit te demonstreren, kunnen we de waarde van yield loggen en next() een paar keer oproepen met enkele waarden.

Dit zal de volgende uitvoer geven:

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

Het is ook mogelijk om de generator te zaaien met een beginwaarde. In het volgende voorbeeld maken we een for lus en geven we elke waarde door aan de next() methode, maar geven we ook een argument door aan de beginfunctie:

We halen de waarde op uit next() en geven een nieuwe waarde door aan de volgende iteratie, die de vorige waarde maal tien is. Dit geeft het volgende:

Output
010203040

Een andere manier om met het opstarten van een generator om te gaan, is om de generator in een functie te wikkelen die next() altijd één keer aanroept voordat hij iets anders doet.

async/await met Generators

Een asynchrone functie is een type functie dat beschikbaar is in ES6+ JavaScript en dat het werken met asynchrone gegevens begrijpelijker maakt door het synchroon te laten lijken. Generatoren hebben een uitgebreidere reeks mogelijkheden dan asynchrone functies, maar zijn in staat om vergelijkbaar gedrag te repliceren. Het op deze manier implementeren van asynchroon programmeren kan de flexibiliteit van uw code vergroten.

In dit gedeelte zullen we een voorbeeld demonstreren van het reproduceren van async/await met generatoren.

Laten we een asynchrone functie bouwen die de Fetch API gebruikt om gegevens op te halen uit de JSONPlaceholder API (die voorbeeld JSON-gegevens biedt voor testdoeleinden) en logt het antwoord naar de console.

Begin met het definiëren van een asynchrone functie genaamd getUsers die gegevens van de API ophaalt en een array van objecten retourneert, roep dan getUsers aan:

Dit zal JSON-gegevens opleveren die lijken op het volgende:

Met behulp van generatoren kunnen we iets bijna identieks maken dat geen gebruik maakt van de async/await trefwoorden. In plaats daarvan zal het een nieuwe functie gebruiken die we maken en yield waarden in plaats van await beloften.

In het volgende codeblok definiëren we een functie genaamd getUsers die onze nieuwe asyncAlt functie gebruikt (die we later zullen schrijven) om async/await na te bootsen.

Zoals we kunnen zien, ziet het er bijna identiek uit als de async/await implementatie, behalve dat er een generator functie wordt doorgegeven die waarden oplevert.

Nu kunnen we een asyncAlt functie maken die lijkt op een asynchrone functie. asyncAlt heeft een generatorfunctie als parameter, die onze functie is die de beloften oplevert die fetch teruggeeft. asyncAlt retourneert zelf een functie, en lost elke belofte die het vindt op tot de laatste:

Dit zal dezelfde uitvoer geven als de async/await versie:

Merk op dat deze implementatie dient om te demonstreren hoe generators kunnen worden gebruikt in plaats van async/await, en geen productieklaar ontwerp is. Er is geen foutafhandeling ingesteld, noch is er de mogelijkheid om parameters mee te geven aan de gegenereerde waarden. Hoewel deze methode flexibiliteit aan uw code kan toevoegen, zal async/await vaak een betere keuze zijn, omdat het implementatiedetails weg abstraheert en u laat concentreren op het schrijven van productieve code.

Conclusie

Generators zijn processen die de uitvoering kunnen stoppen en hervatten. Ze zijn een krachtige, veelzijdige functie van JavaScript, hoewel ze niet vaak worden gebruikt. In deze tutorial hebben we geleerd over generatorfuncties en generatorobjecten, methoden die beschikbaar zijn voor generatoren, de operatoren yield en yield*, en generatoren die worden gebruikt met eindige en oneindige gegevensverzamelingen. We hebben ook een manier verkend om asynchrone code te implementeren zonder geneste callbacks of lange belofteketens.

Als u meer wilt leren over de syntaxis van JavaScript, kijk dan eens naar onze tutorials Understanding This, Bind, Call, and Apply in JavaScript en Understanding Map and Set Objects in JavaScript.

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.