En snabb introduktion

Välkommen till den underbara världen av generatorfunktioner och asynkrona generatorer i JavaScript.

Du är på väg att lära dig mer om en av de mest exotiska (enligt de flesta utvecklare) JavaScript-funktionerna.

Vi sätter igång!

Generatorfunktioner i JavaScript

Vad är en generatorfunktion?

En generatorfunktion (ECMAScript 2015) i JavaScript är en speciell typ av synkron funktion som kan stoppa och återuppta sin exekvering när som helst.

I motsats till vanliga JavaScript-funktioner, som är eld och glömma, har generatorfunktioner också förmågan att:

  • kommunicera med anroparen över en dubbelriktad kanal.
  • behålla sin exekveringskontext (scope) över efterföljande anrop.

Du kan se generatorfunktioner som closures på steroider, men likheterna slutar här!

Din första generatorfunktion

För att skapa en generatorfunktion sätter vi en stjärna * efter nyckelordet function:

function* generate() {//}

Observera att generatorfunktioner också kan anta formen av en klassmetod, eller av ett funktionsuttryck. Däremot är pilgeneratorfunktioner inte tillåtna.

När vi väl är inne i funktionen kan vi använda yield för att pausa exekveringen:

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

yield pausar exekveringen och returnerar ett s.k. Generator-objekt till den som anropar. Detta objekt är både en iterabel och en iterator på samma gång.

Låt oss avmystifiera dessa begrepp.

Iterabler och iteratorer

Ett iterabelt objekt i JavaScript är ett objekt som implementerar Symbol.iterator. Här är ett minimalt exempel på iterable:

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

När vi väl har det här iterable-objektet tilldelar vi en funktion till för att returnera ett iterator-objekt.

Det här låter som mycket teori, men i praktiken är ett iterable-objekt ett objekt som vi kan slinga oss över med for...of (ECMAScript 2015). Mer om detta om en minut.

Du bör redan känna till ett par iterables i JavaScript: matriser och strängar är till exempel iterables:

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

Andra kända iterables är Map och Set. for...of kommer också väl till pass för iteration över ett objekts värden:

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

Vänligen kom ihåg att alla egenskaper som är markerade som enumerable: false inte kommer att dyka upp 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 är problemet med vår anpassade iterabel att den inte kan gå långt ensam utan en iterator.

Iteratorer är också objekt, men de bör följa iteratorprotokollet. Kortfattat kan man säga att iteratorer måste ha minst en next()-metod.

next() måste returnera ett annat objekt, vars egenskaper är value och done.

Logiken för vår next()-metod måste följa följande regler:

  • Vi returnerar ett objekt med done: false för att fortsätta iterationen.
  • Vi returnerar ett objekt med done: true för att stoppa iterationen.

value bör istället innehålla det resultat som vi vill producera för konsumenten.

Låt oss utöka vårt exempel genom att lägga till iteratorn:

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

Här har vi en iterabel, som korrekt implementerar Symbol.iterator. Vi har också en iterator, som returnerar:

  • ett objekt vars form är { value: x, done: false} tills count når 3.
  • ett objekt vars form är { value: x, done: true} när count når 3.

Denna minimala iterabel är redo att itereras 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 blir:

123

När du lär känna generatorfunktioner bättre kommer du att se att iteratorer är grunden för generatorobjekt.

Håll dig i minnet:

  • iterabler är de objekt som vi itererar över.
  • iteratorer är de saker som gör iterabeln … ”

I slutändan, vad är poängen med en iterabel?

Vi har nu en standardiserad, pålitlig for...ofslinga som fungerar praktiskt taget för nästan alla datastrukturer, anpassade eller inhemska, i JavaScript.

För att använda for...of på din anpassade datastruktur måste du:

  • implementera Symbol.iterator.
  • tillhandahålla ett iteratorobjekt.

Det var allt!

Framtida resurser:

  • Iteratorer kommer att iterera av Jake Archibald.

Iterables are spreadable and destructurable

In addition to for...of, we can also use spread and destructuring on finite iterables.

Konsultera återigen det tidigare exemplet:

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

För att dra ut alla värden kan vi sprida iterabeln till en array:

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

För att dra ut bara ett par värden i stället kan vi array-destrukturera iterabeln. Här får vi ut det första och andra värdet från iterable:

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

Här får vi istället ut det första, och det tredje:

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

Låt oss nu återigen vända vårt fokus på generatorfunktioner.

Utdragning av data från en generatorfunktion

När en generatorfunktion väl är på plats kan vi börja interagera med den. Denna interaktion består av:

  • hämta värden från generatorn, steg för steg.
  • Operativt skicka värden tillbaka till generatorn.

För att hämta värden ur en generator kan vi använda tre tillvägagångssätt:

  • kalla next() på iteratorobjektet.
  • iteration med for...of.
  • spridning och array-destrukturering.

En generatorfunktion beräknar inte alla sina resultat i ett enda steg, som vanliga funktioner gör.

Om vi tar vårt exempel, för att få värden från generatorn kan vi först och främst värma upp generatorn:

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

Här blir go vårt iterable/iteratorobjekt, resultatet av anropet av generate.

(Kom ihåg att ett Generator-objekt är både en iterabel och en iterator).

Från och med nu kan vi anropa go.next() för att föra exekveringen framåt:

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

Här producerar varje anrop till go.next() ett nytt objekt. I exemplet destruerar vi egenskapen value från detta objekt.

Objekt som returneras från anrop next() på iteratorobjektet har två egenskaper:

  • value: Värdet för det aktuella steget.
  • done: Ett booleanskt värde som anger om det finns fler värden i generatorn, eller inte.

Vi implementerade ett sådant iteratorobjekt i föregående avsnitt. När du använder en generator finns iteratorobjektet redan där för dig.

next() fungerar bra för att extrahera ändliga data från ett iteratorobjekt.

För att iterera över icke ändliga data kan vi använda for...of. Här är en ändlös generator:

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

Som du märker finns det inget behov av att initialisera generatorn när du använder for...of.

Slutligt kan vi också sprida och destruera generatorobjektet. Här är spridningen:

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

Här är destruktureringen:

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

Det är viktigt att notera att generatorer går ut när du förbrukar alla deras värden.

Om du sprider ut värden från en generator finns det inget kvar att dra ut efteråt:

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 fungerar också åt andra hållet: de kan ta emot värden eller kommandon från anroparen, vilket vi kommer att se om en minut.

Att prata tillbaka till generatorfunktioner

Iteratorobjekt, det resulterande objektet från ett anrop av en generatorfunktion, exponerar följande metoder:

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

Vi har redan sett next() som hjälper till med att dra ut objekt från en generator.

Den används inte bara för att hämta ut data, utan även för att skicka värden till generatorn.

Tänk på följande generator:

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

Den listar en parameter string. Vi tillhandahåller detta argument vid initialiseringen:

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

Sedan vi anropar next() för första gången på iteratorobjektet startar exekveringen och producerar ”A”:

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

Vid denna tidpunkt kan vi prata tillbaka till generatorn genom att tillhandahålla ett argument för next:

go.next("b");

Här är hela listan:

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

Från och med nu kan vi mata värden till yield varje gång vi behöver en ny stor bokstavssträng:

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

Om vi när som helst vill återvända från exekveringen helt och hållet kan vi använda return på iteratorobjektet:

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

Detta stoppar exekveringen.

Förutom värden kan du också kasta ett undantag i generatorn. Se ”Felhantering för generatorfunktioner”.

Användningsområden för generatorfunktioner

De flesta utvecklare (inklusive mig) ser generatorfunktioner som en exotisk JavaScript-funktion som har liten eller ingen tillämpning i den verkliga världen.

Detta kan vara sant för det genomsnittliga front-end arbetet, där ett stänk av jQuery och lite CSS kan göra susen för det mesta.

I verkligheten briljerar generatorfunktioner verkligen i alla de scenarier där prestationer är av största vikt.

I synnerhet är de bra för:

  • arbeta med stora filer och datamängder.
  • datahantering i back-end, eller i front-end.
  • generera oändliga sekvenser av data.
  • beräkna dyr logik på begäran.

Generatorfunktioner är också byggstenen för sofistikerade asynkrona mönster med asynkrona generatorfunktioner, vårt ämne för nästa avsnitt.

Asynkrona generatorfunktioner i JavaScript

Vad är en asynkron generatorfunktion?

En asynkron generatorfunktion (ECMAScript 2018) är en speciell typ av asynkron funktion som kan stoppa och återuppta sin exekvering efter behag.

Skillnaden mellan synkrona generatorfunktioner och asynkrona generatorfunktioner är att de sistnämnda returnerar ett asynkront, löftesbaserat resultat från iteratorobjektet.

Som generatorfunktioner kan asynkrona generatorfunktioner:

  • kommunicera med den som anropar.
  • behålla sin exekveringskontext (scope) över efterföljande anrop.

Din första asynkrona generatorfunktion

För att skapa en asynkron generatorfunktion deklarerar vi en generatorfunktion med stjärnan * med prefix async:

async function* asyncGenerator() { //}

När vi är inne i funktionen kan vi använda yield för att pausa exekveringen:

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

Här pausar yield exekveringen och returnerar ett s.k. Generator-objekt till anroparen.

Detta objekt är både en iterabel och en iterator på samma gång.

Låt oss sammanfatta dessa begrepp för att se hur de passar in i det asynkrona landet.

Asynkrona iterabler och iteratorer

En asynkron iterabel i JavaScript är ett objekt som implementerar Symbol.asyncIterator.

Här är ett minimalt exempel:

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

När vi har det här iterable-objektet tilldelar vi en funktion till för att returnera ett iteratorobjekt.

Iteratorobjektet ska överensstämma med iteratorprotokollet med en next()-metod (som den synkrona iteratorn).

Låt oss utöka vårt exempel genom att lägga till iteratorn:

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

Den här iteratorn liknar den som vi byggde i de tidigare avsnitten, den här gången är den enda skillnaden att vi lindar in det återkommande objektet med Promise.resolve.

I det här läget kan vi göra något i stil med:

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

Och med for await...of:

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

Asynkrona iterabler och iteratorer är grunden för asynkrona generatorfunktioner.

Låt oss nu återigen vända vårt fokus på dem.

Utvinning av data från en asynkron generator

Asynkrona generatorfunktioner beräknar inte alla sina resultat i ett enda steg, som vanliga funktioner gör.

Istället drar vi ut värden steg för steg.

Efter att ha undersökt asynkrona iteratorer och iterables borde det inte vara någon överraskning att se att för att dra ut löften ur en asynkron generator kan vi använda oss av två tillvägagångssätt:

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

I vårt första exempel kan vi göra:

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

Utgången från den här koden är:

3399

Det andra tillvägagångssättet använder async iteration med for await...of. För att använda async iteration lindar vi in konsumenten med en async-funktion.

Här är det kompletta exemplet:

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

for await...of fungerar bra för att extrahera icke-ändliga dataströmmar.

Här ser vi nu hur vi skickar data tillbaka till generatorn.

Talka tillbaka till asynkrona generatorfunktioner

Konsultera följande asynkrona generator:

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

Som den oändliga upphöjningsmaskinen från generatorexemplet kan vi ge ett argument till 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();

Här skickas ett nytt värde in i generatorn i varje steg.

Utgången av den här koden är:

ABC

Även om det inte finns något i sig asynkront i toUpperCase() kan man ana syftet med det här mönstret.

Om vi när som helst vill avsluta utförandet kan vi anropa return() på iteratorobjektet:

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

Förutom värden kan du också kasta ett undantag i generatorn. Se ”Felhantering för asynkrona generatorer”.

Användningsfall för asynkrona iterables och asynkrona generatorfunktioner

Om generatorfunktioner är bra för att arbeta synkront med stora filer och oändliga sekvenser, möjliggör asynkrona generatorfunktioner ett helt nytt land av möjligheter för JavaScript.

I synnerhet asynkron iteration gör det lättare att konsumera läsbara strömmar. Objektet Response i Fetch exponerar body som en läsbar ström med getReader(). Vi kan omsluta en sådan ström med en asynkron generator och senare iterera över den med for await...of.

Async iterators and generators av Jake Archibald har ett gäng fina exempel.

Andra exempel på strömmar är begäranströmmar med Fetch.

I skrivande stund finns det inget webbläsar-API som implementerar Symbol.asyncIterator, men Stream-specifikationen kommer att ändra på detta.

In Node.js Det senaste Stream API:et fungerar bra med asynkrona generatorer och asynkron iteration.

I framtiden kommer vi att kunna konsumera och arbeta sömlöst med skrivbara, läsbara och transformatoriska strömmar på klientsidan.

Framtida resurser:

  • The Stream API.

Sammanfattning

Nyckeltermer och begrepp som vi tog upp i det här inlägget:

ECMAScript 2015:

  • iterable.
  • iterator.

Dessa är byggstenarna för generatorfunktioner.

ECMAScript 2018:

  • asynkron iterable.
  • asynkron iterator.

Dessa är istället byggstenarna för asynkrona generatorfunktioner.

En god förståelse för iterables och iteratorer kan ta dig långt. Det är inte så att du kommer att arbeta med generatorfunktioner och asynkrona generatorfunktioner varje dag, men de är en trevlig färdighet att ha i sitt verktygsbälte.

Och du? Har du någonsin använt dem?

Tack för att du läste och håll ögonen öppna på den här bloggen!

Lämna ett svar

Din e-postadress kommer inte publiceras.