En hurtig introduktion

Velkommen til den vidunderlige verden af generatorfunktioner og asynkrone generatorer i JavaScript.

Du er ved at lære om en af de mest eksotiske (ifølge de fleste udviklere) JavaScript-funktioner.

Lad os komme i gang!

Generatorfunktioner i JavaScript

Hvad er en generatorfunktion?

En generatorfunktion (ECMAScript 2015) i JavaScript er en særlig type synkron funktion, som er i stand til at stoppe og genoptage sin udførelse efter behag.

I modsætning til almindelige JavaScript-funktioner, som er fire and forget, har generatorfunktioner også mulighed for:

  • at kommunikere med den kaldende part over en tovejs-kanal.
  • at bevare deres eksekveringskontekst (scope) over efterfølgende kald.

Du kan tænke på generatorfunktioner som på closures på steroider, men lighederne stopper her!

Din første generatorfunktion

For at oprette en generatorfunktion sætter vi en stjerne * efter nøgleordet function:

function* generate() {//}

Bemærk: Generatorfunktioner kan også antage form af en klassemetode eller af et funktionsudtryk. Derimod er pilgeneratorfunktioner ikke tilladt.

Når vi er inde i funktionen, kan vi bruge yield til at sætte eksekveringen på pause:

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

yield sætter eksekveringen på pause og returnerer et såkaldt Generator-objekt til den, der kalder den. Dette objekt er både en iterabel og en iterator på samme tid.

Lad os afmystificere disse begreber.

Iterabler og iteratorer

Et iterabelt objekt i JavaScript er et objekt, der implementerer Symbol.iterator. Her er et minimalt eksempel på en iterable:

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

Når vi har dette iterable-objekt, tildeler vi en funktion til for at returnere et iterator-objekt.

Det lyder som en masse teori, men i praksis er en iterable et objekt, som vi kan løbe over med for...of (ECMAScript 2015). Mere om dette om et øjeblik.

Du bør allerede kende et par iterables i JavaScript: Arrays og strings er for eksempel iterables:

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

Andre kendte iterables er Map og Set. for...of er også praktisk til iteration over et objekts værdier:

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

Husk, at enhver egenskab markeret som enumerable: false ikke vises i iterationen:

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

Nu er problemet med vores brugerdefinerede iterabel, at den ikke kan komme langt alene uden en iterator.

Iteratorer er også objekter, men de skal overholde iteratorprotokollen. Kort fortalt skal iteratorer mindst have en next() metode.

next() skal returnere et andet objekt, hvis egenskaber er value og done.

Logikken for vores next()-metode skal overholde følgende regler:

  • Vi returnerer et objekt med done: false for at fortsætte iterationen.
  • Vi returnerer et objekt med done: true for at stoppe iterationen.

value skal i stedet indeholde det resultat, som vi ønsker at producere til forbrugeren.

Lad os udvide vores eksempel ved at tilføje iteratoren:

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

Her har vi en iterabel, som korrekt implementerer Symbol.iterator. Vi har også en iterator, som returnerer:

  • en genstand, hvis form er { value: x, done: false}, indtil count når 3.
  • en genstand, hvis form er { value: x, done: true}, når count når 3.

Denne minimale iterabel er klar til at blive itereret med 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);}

Resultatet vil være:

123

Når du lærer generatorfunktioner bedre at kende, vil du se, at iteratorer er fundamentet for generatorobjekter.

Hold dig for øje:

  • iterables er de objekter, som vi itererer over.
  • iteratorer er de ting, der gør iterablen … “loopable” over.

I sidste ende, hvad er pointen med en iterabel?

Vi har nu et standardiseret, pålideligt for...of loop, der fungerer stort set for næsten alle datastrukturer, brugerdefinerede eller native, i JavaScript.

For at bruge for...of på din brugerdefinerede datastruktur skal du:

  • implementere Symbol.iterator.
  • tilvejebringe et iteratorobjekt.

Det var det!

Flere ressourcer:

  • Iterators gonna iterate af Jake Archibald.

Iterables kan spredes og destrueres

I tillæg til for...of kan vi også bruge spredning og destruktion på finitte iterables.

Opnå igen det forrige eksempel:

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

For at trække alle værdierne ud kan vi sprede iterablen til et array:

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

For at trække kun et par værdier ud i stedet kan vi array-destrukturere iterablen. Her får vi den første og anden værdi fra iterablen:

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

Her får vi i stedet den første, og den tredje:

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

Lad os nu igen vende vores fokus mod generatorfunktioner.

Udtrække data fra en generatorfunktion

Når en generatorfunktion er på plads, kan vi begynde at interagere med den. Denne interaktion består i:

  • at hente værdier fra generatoren, trin for trin.
  • Optionelt kan vi sende værdier tilbage til generatoren.

For at trække værdier ud af en generator kan vi bruge tre tilgange:

  • Kald next() på iteratorobjektet.
  • iteration med for...of.
  • spredning og array-destrukturering.

En generatorfunktion beregner ikke alle sine resultater i et enkelt trin, som almindelige funktioner gør.

Hvis vi tager vores eksempel, for at få værdier fra generatoren, kan vi først og fremmest varme generatoren op:

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

Her bliver go vores iterable/iteratorobjekt, resultatet af at kalde generate.

(Husk, at et Generator-objekt er både en iterabel og en iterator).

Fra nu af kan vi kalde go.next() for at fremme udførelsen:

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

Her producerer hvert kald til go.next() et nyt objekt. I eksemplet destruerer vi egenskaben value fra dette objekt.

Objekter, der returneres ved at kalde next() på iteratorobjektet, har to egenskaber:

  • value: værdien for det aktuelle trin.
  • done: et boolean, der angiver, om der er flere værdier i generatoren eller ej.

Vi implementerede et sådant iteratorobjekt i det foregående afsnit. Når du bruger en generator, er iteratorobjektet allerede til stede for dig.

next() fungerer godt til at udtrække finitte data fra et iteratorobjekt.

For at iterere over ikke-finitte data kan vi bruge for...of. Her er en endeløs generator:

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

Som du kan se, er der ikke behov for at initialisere generatoren, når du bruger for...of.

Endeligt kan vi også sprede og destrukturere generatorobjektet. Her er spredningen:

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

Her er destruktureringen:

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

Det er vigtigt at bemærke, at generatorer bliver udtømt, når du forbruger alle deres værdier.

Hvis du spreder værdier fra en generator, er der intet tilbage at trække ud bagefter:

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

Generatorer fungerer også den anden vej rundt: De kan acceptere værdier eller kommandoer fra den, der kalder dem, som vi vil se om et øjeblik.

Vi taler tilbage til generatorfunktioner

Iteratorobjekter, det resulterende objekt af et kald af en generatorfunktion, eksponerer følgende metoder:

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

Vi har allerede set next(), som hjælper med at trække objekter ud af en generator.

Denne anvendelse er ikke kun begrænset til at trække data ud, men også til at sende værdier til generatoren.

Se på følgende generator:

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

Den opregner en parameter string. Vi giver dette argument ved initialiseringen:

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

Så snart vi kalder next() for første gang på iteratorobjektet, starter udførelsen og producerer “A”:

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

På dette tidspunkt kan vi tale tilbage til generatoren ved at give et argument for next:

go.next("b");

Her er den komplette liste:

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

Fra nu af kan vi fodre værdier til yield, hver gang vi har brug for en ny streng med store bogstaver:

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

Hvis vi på et hvilket som helst tidspunkt ønsker at vende helt tilbage fra eksekveringen, kan vi bruge return på iteratorobjektet:

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

Dette stopper eksekveringen.

Ud over værdier kan du også kaste en undtagelse ind i generatoren. Se “Fejlhåndtering af generatorfunktioner”.

Anvendelsesområder for generatorfunktioner

De fleste udviklere (mig selv inklusive) ser generatorfunktioner som en eksotisk JavaScript-funktion, der kun har ringe eller ingen anvendelse i den virkelige verden.

Det kan være rigtigt for det gennemsnitlige front-end arbejde, hvor et drys jQuery og en smule CSS kan gøre tricket de fleste gange.

I virkeligheden skinner generatorfunktioner virkelig i alle de scenarier, hvor præstationer er altafgørende.

I særdeleshed er de gode til:

  • arbejde med store filer og datasæt.
  • data-wrangling i back-end eller i front-end.
  • generering af uendelige sekvenser af data.
  • komputere dyr logik on-demand.

Generatorfunktioner er også byggestenene til sofistikerede asynkrone mønstre med asynkrone generatorfunktioner, vores emne i næste afsnit.

Asynkrone generatorfunktioner i JavaScript

Hvad er en asynkron generatorfunktion?

En asynkron generatorfunktion (ECMAScript 2018) er en særlig type asynkron funktion, som er i stand til at stoppe og genoptage sin udførelse efter behag.

Den forskel mellem synkrone generatorfunktioner og asynkrone generatorfunktioner er, at sidstnævnte returnerer et asynkront, løftebaseret resultat fra iteratorobjektet.

Som generatorfunktioner er asynkrone generatorfunktioner i stand til:

  • at kommunikere med den, der kalder.
  • bevare deres eksekveringskontekst (scope) over efterfølgende kald.

Din første asynkrone generatorfunktion

For at oprette en asynkron generatorfunktion erklærer vi en generatorfunktion med stjernen *, med præfikset async:

async function* asyncGenerator() { //}

Når vi er inde i funktionen, kan vi bruge yield til at sætte eksekveringen på pause:

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

Her sætter yield eksekveringen på pause og returnerer et såkaldt Generator-objekt til den, der kalder funktionen.

Dette objekt er både en iterabel og en iterator på samme tid.

Lad os rekapitulere disse begreber for at se, hvordan de passer ind i det asynkrone land.

Asynkrone iterabler og iteratorer

En asynkron iterabel i JavaScript er et objekt, der implementerer Symbol.asyncIterator.

Her er et minimalt eksempel:

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

Når vi har dette iterable-objekt, tildeler vi en funktion til for at returnere et iterator-objekt.

Itératorobjektet skal overholde iteratorprotokollen med en next()-metode (ligesom den synkrone iterator).

Lad os udvide vores eksempel ved at tilføje iteratoren:

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

Denne iterator svarer til den, vi byggede i de foregående afsnit, denne gang er den eneste forskel, at vi indpakker det returnerende objekt med Promise.resolve.

På dette tidspunkt kan vi gøre noget i denne retning:

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

Og med for await...of:

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

Asynkrone iterables og iteratorer er fundamentet for asynkrone generatorfunktioner.

Lad os nu igen vende vores fokus mod dem.

Udvinding af data fra en asynkron generator

Asynkrone generatorfunktioner beregner ikke alle deres resultater i et enkelt trin, som almindelige funktioner gør.

I stedet trækker vi værdier ud trin for trin.

Efter at have undersøgt asynkrone iteratorer og iterables bør det ikke være nogen overraskelse at se, at vi kan bruge to fremgangsmåder til at trække Promises ud af en asynkron generator:

  • Kald next() på iteratorobjektet.
  • async-iteration med for await...of.

I vores første eksempel kan vi gøre:

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

Opdatet fra denne kode er:

3399

Den anden tilgang bruger async-iteration med for await...of. For at bruge async-iteration pakker vi forbrugeren ind i en async-funktion.

Her er det komplette eksempel:

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

for await...of fungerer fint til at udtrække ikke-endelige strømme af data.

Lad os nu se, hvordan vi sender data tilbage til generatoren.

Taler vi tilbage til asynkrone generatorfunktioner

Opmærksom på følgende asynkrone generator:

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

Som den endeløse storskriftmaskine fra generatoreksemplet kan vi give et argument til 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();

Her sender hvert trin en ny værdi ind i generatoren.

Udgangen af denne kode er:

ABC

Selv om der ikke er noget iboende asynkront i toUpperCase(), kan man fornemme formålet med dette mønster.

Hvis vi på et hvilket som helst tidspunkt ønsker at forlade udførelsen, kan vi kalde return() på iteratorobjektet:

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

Ud over værdier kan du også kaste en undtagelse ind i generatoren. Se “Fejlhåndtering for asynkrone generatorer”.

Anvendelsestilfælde for asynkrone iterables og asynkrone generatorfunktioner

Hvis generatorfunktioner er gode til at arbejde synkront med store filer og uendelige sekvenser, giver asynkrone generatorfunktioner mulighed for et helt nyt land af muligheder for JavaScript.

I særdeleshed gør asynkron iteration det lettere at forbruge læsbare strømme. Response-objektet i Fetch eksponerer body som en læsbar strøm med getReader(). Vi kan indpakke en sådan strøm med en asynkron generator og senere iterere over den med for await...of.

Async iterators and generators af Jake Archibald har en masse gode eksempler.

Andre eksempler på streams er request streams med Fetch.

I skrivende stund er der ikke noget browser-API, der implementerer Symbol.asyncIterator, men Stream-specifikationen vil ændre dette.

In Node.js den seneste Stream API spiller fint sammen med asynkrone generatorer og asynkron iteration.

I fremtiden vil vi være i stand til at forbruge og arbejde problemfrit med skrivbare, læsbare og transformerede streams på klientsiden.

Flere ressourcer:

  • The Stream API.

Opsummering

Nøglebegreber og begreber, som vi har behandlet i dette indlæg:

ECMAScript 2015:

  • iterable.
  • iterator.

Disse er byggestenene til generatorfunktioner.

ECMAScript 2018:

  • asynkron iterabel.
  • asynkron iterator.

Disse er i stedet byggestenene til asynkrone generatorfunktioner.

En god forståelse af iterables og iteratorer kan føre dig langt. Det er ikke fordi du kommer til at arbejde med generatorfunktioner og asynkrone generatorfunktioner hver dag, men de er en god færdighed at have i dit værktøjsbælte.

Og du? Har du nogensinde brugt dem?

Tak for læsning og bliv ved med at følge med på denne blog!

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.