A generátorok megértése JavaScriptben

A szerző a Write for DOnations program keretében a Nyílt Internet/Szólásszabadság Alapot választotta adományozásra.

Bevezetés

Az ECMAScript 2015-ben a generátorok bekerültek a JavaScript nyelvbe. A generátor olyan folyamat, amely szüneteltethető és folytatható, és több értéket is eredményezhet. A generátor a JavaScriptben egy generátorfüggvényből áll, amely egy iterálható Generator objektumot ad vissza.

A generátorok képesek állapotot fenntartani, így hatékony módot biztosítanak az iterátorok készítésére, és képesek végtelen adatfolyamok kezelésére, ami felhasználható végtelen görgetés megvalósítására egy webes alkalmazás frontendjén, hanghullám-adatokkal való működésre és sok másra. Továbbá, ha Promises-szel együtt használjuk, a generátorok utánozhatják a async/await funkcionalitást, ami lehetővé teszi, hogy egyszerűbb és olvashatóbb módon kezeljük az aszinkron kódot. Bár a async/await egy elterjedtebb módja a gyakori, egyszerű aszinkron felhasználási esetek kezelésének, mint például az adatok lekérése egy API-ból, a generátorok fejlettebb funkciókkal rendelkeznek, amelyek miatt érdemes megtanulni a használatukat.

Ebben a cikkben foglalkozunk a generátorfüggvények létrehozásával, a Generator objektumok iterálásával, a yield és return közötti különbséggel egy generátoron belül, valamint a generátorokkal való munka egyéb aspektusaival.

Generátorfüggvények

A generátorfüggvény olyan függvény, amely egy Generator objektumot ad vissza, és amelyet a function kulcsszó, majd egy csillag (*) határoz meg, ahogy az alábbiakban látható:

// Generator function declarationfunction* generatorFunction() {}

Egyszer a csillagot a függvény neve mellett látjuk, szemben a függvény kulcsszóval, például function *generatorFunction(). Ez ugyanúgy működik, de a function* egy szélesebb körben elfogadott szintaxis.

A generátorfüggvényeket egy kifejezésben is definiálhatjuk, mint a reguláris függvényeket:

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

A generátorok akár egy objektum vagy osztály metódusai is lehetnek:

A cikkben szereplő példákban a generátorfüggvény deklarációs szintaxist fogjuk használni.

Figyelem: A normál függvényekkel ellentétben a generátorok nem konstruálhatók a new kulcsszóval, és nem használhatók nyílfüggvényekkel együtt.

Most, hogy tudjuk, hogyan kell deklarálni a generátorfüggvényeket, nézzük meg az általuk visszaadott iterálható Generator objektumokat.

Generátorobjektumok

Hagyományosan a JavaScriptben a függvények a befejezésig futnak, és a függvény meghívása egy értéket ad vissza, amikor a return kulcsszóhoz érkezik. Ha a return kulcsszó kimarad, a függvény implicit módon undefined értéket ad vissza.

A következő kódban például deklarálunk egy sum() függvényt, amely egy olyan értéket ad vissza, amely két egész szám argumentum összege:

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

A függvény meghívása egy olyan értéket ad vissza, amely az argumentumok összege:

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

A generátorfüggvény azonban nem ad vissza azonnal értéket, hanem egy iterálható Generator objektumot ad vissza. A következő példában deklarálunk egy függvényt, és egyetlen visszatérési értéket adunk neki, mint egy hagyományos függvénynek:

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

Mikor meghívjuk a generátorfüggvényt, az a Generator objektumot adja vissza, amelyet egy változóhoz rendelhetünk:

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

Ha ez egy hagyományos függvény lenne, akkor a generator-től várnánk a függvényben visszaadott stringet. Valójában azonban egy suspended állapotú objektumot kapunk. A generator meghívása tehát a következőhöz hasonló kimenetet ad:

A függvény által visszaadott Generator objektum egy iterátor. Az iterátor olyan objektum, amely rendelkezik egy next() metódussal, amely az értékek sorozatának iterálására szolgál. A next() metódus egy value és done tulajdonságokkal rendelkező objektumot ad vissza. A value a visszaadott értéket jelenti, a done pedig azt, hogy az iterátor végigfutott-e az összes értékén vagy sem.

Ezek ismeretében hívjuk meg a next()-et a generator-ünkön, és kapjuk meg az iterátor aktuális értékét és állapotát:

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

Ez a következő kimenetet adja:

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

A next() hívása által visszaadott érték Hello, Generator!, a done állapota pedig true, mivel ez az érték egy return-ból származik, amely lezárta az iterátort. Mivel az iterátor befejeződött, a generátorfüggvény állapota suspended-ról closed-re változik. A generator ismételt meghívása a következőket fogja eredményezni:

Output
generatorFunction {<closed>}

Most csak azt mutattuk be, hogy a generátorfüggvény egy összetettebb módja annak, hogy egy függvény return értékét megkapjuk. A generátorfüggvényeknek azonban egyedi tulajdonságaik is vannak, amelyek megkülönböztetik őket a normál függvényektől. A következő részben megismerkedünk a yield operátorral, és megnézzük, hogyan képes egy generátor szüneteltetni és folytatni a végrehajtást.

yield operátorok

A generátorok egy új kulcsszót vezetnek be a JavaScriptbe: yield. A yield képes szüneteltetni egy generátorfüggvényt, és visszaadni a yield után következő értéket, így könnyített módot biztosít az értékek iterálására.

Ebben a példában háromszor fogjuk szüneteltetni a generátorfüggvényt különböző értékekkel, és a végén visszaadunk egy értéket. Ezután a Generator objektumunkat hozzárendeljük a generator változóhoz.

Most, amikor meghívjuk a next() generátorfüggvényt, az minden alkalommal szünetet tart, amikor találkozik a yield értékkel. A done minden yield után false lesz, jelezve, hogy a generátor még nem fejezte be. Amint találkozik egy return-zal, vagy a függvényben nem találkozik több yield-vel, a done true-re vált, és a generátor befejeződik.

A next() metódust négyszer egymás után használjuk:

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

Ezek sorrendben a következő négy sor kimenetet adják:

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

Megjegyezzük, hogy a generátornak nem kell return; ha elhagyjuk, az utolsó iteráció {value: undefined, done: true}-et fog visszaadni, ahogy a generátor befejezése után a next() minden további hívása is.

Iterálás egy generátoron

A next() metódus segítségével manuálisan iteráltuk végig a Generator objektumot, megkapva a teljes objektum összes value és done tulajdonságát. Azonban a Array, Map és Set objektumokhoz hasonlóan egy Generator is követi az iterációs protokollt, és a for...of segítségével iterálható végig:

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

Ez a következőket adja vissza:

Output
NeoMorpheusTrinity

A szórás operátorral egy Generator objektum értékeit is hozzárendelhetjük egy tömbhöz.

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

Ez a következő tömböt adja:

Output
(3)

Mind a spread, mind a for...of nem fogja a return-t beletenni az értékekbe (ebben az esetben 'The Oracle' lett volna).

Megjegyzés: Míg mindkét módszer hatékony véges generátorokkal való munkához, ha egy generátor végtelen adatfolyamot kezel, nem lesz lehetséges a spread vagy a for...of közvetlen használata végtelen ciklus létrehozása nélkül.

Generátor lezárása

Mint láttuk, egy generátor done tulajdonságát true-re, állapotát pedig closed-re állíthatjuk az összes értékének iterálásával. A generátor azonnali megszüntetésének további két módja van: a return() módszerrel és a throw() módszerrel.

A return() módszerrel a generátor bármelyik ponton megszüntethető, ugyanúgy, mintha a függvénytestben egy return utasítás lenne. A return()-ba átadhatunk egy argumentumot, vagy üresen hagyhatjuk, ha nem definiált értékről van szó.

A return() demonstrálására létrehozunk egy generátort néhány yield értékkel, de return nélkül a függvénydefinícióban:

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

Az első next() adja a 'Neo'-et, a done pedig a false-et. Ha közvetlenül ezután meghívunk egy return() metódust a Generator objektumon, akkor most az átadott értéket kapjuk, és done true-re lesz állítva. A next() bármely további hívása a generátor alapértelmezett befejezett válaszát fogja adni egy meghatározatlan értékkel.

Az alábbi három metódust futtassuk le a generator-en:

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

Ez a következő három eredményt fogja adni:

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

A return() metódus arra kényszerítette a Generator objektumot, hogy befejezze és figyelmen kívül hagyjon minden más yield kulcsszót. Ez különösen hasznos az aszinkron programozásban, amikor függvényeket kell törölhetővé tenni, például megszakítani egy webes kérést, amikor a felhasználó egy másik műveletet akar végrehajtani, mivel egy Promise-t nem lehet közvetlenül törölni.

Ha a generátorfüggvény testében van mód a hibák fogására és kezelésére, akkor a throw() metódussal hibát dobhatunk a generátorba. Ez elindítja a generátort, bedobja a hibát, és befejezi a generátort.

Azért, hogy ezt demonstráljuk, egy try...catch-ot teszünk a generátorfüggvény testébe, és naplózunk egy hibát, ha találunk egyet:

Most futtatjuk a next() metódust, majd a throw()-et:

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

Ez a következő kimenetet fogja adni:

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

A throw() használatával hibát injektáltunk a generátorba, amelyet a try...catch elkapott és naplózott a konzolra.

Generátor objektum metódusok és állapotok

A következő táblázat a Generator objektumokon használható metódusok listáját mutatja:

Módszer Megnevezés
next() A következő értéket adja vissza a generátorban
return() A következő értéket adja vissza a generátorban
return() . egy generátorban és befejezi a generátort
throw() Hibát dob és befejezi a generátort

A következő táblázat a Generator objektum lehetséges állapotait sorolja fel:

Status leírás
suspended A generátor leállította a végrehajtást, de nem fejezte be
closed A generátor vagy hiba miatt befejezte, visszatérés, vagy az összes érték iterálása

yield Delegálás

A szokásos yield operátoron kívül a generátorok a yield* kifejezéssel további értékeket delegálhatnak egy másik generátornak. Amikor a yield* kifejezéssel találkozunk egy generátoron belül, az a delegált generátoron belülre kerül, és elkezdi az összes yield-ek iterálását, amíg a generátor le nem zárul. Ez használható a különböző generátorfüggvények elkülönítésére, hogy szemantikailag rendszerezze a kódot, miközben az összes yield-jük a megfelelő sorrendben iterálható.

A demonstrációhoz létrehozhatunk két generátorfüggvényt, amelyek közül az egyik yield* a másikra fog yield*működni:

A következőkben iteráljuk végig a begin() generátorfüggvényt:

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

Ez a következő értékeket adja a generálás sorrendjében:

Output
1234

A külső generátor a 1 és 2 értékeket adta, majd delegálta a másik generátorra yield*, amely 3 és 4 értékeket adott vissza.

yield* bármely iterálható objektumra, például egy Array-re vagy egy Map-re is delegálhat. A hozamdelegálás hasznos lehet a kód szervezésében, mivel egy generátoron belül minden olyan függvénynek, amely a yield-t akarja használni, szintén generátornak kell lennie.

Végtelen adatfolyamok

A generátorok egyik hasznos tulajdonsága, hogy végtelen adatfolyamokkal és gyűjteményekkel dolgozhatunk. Ez demonstrálható egy végtelen ciklus létrehozásával egy generátorfüggvényen belül, amely egy számot eggyel növel.

A következő kódblokkban definiáljuk ezt a generátorfüggvényt, majd elindítjuk a generátort:

Most az next() segítségével iteráljuk végig az értékeket:

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

Ez a következő kimenetet adja:

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

A függvény a végtelen ciklusban egymást követő értékeket ad vissza, miközben a done tulajdonság false marad, biztosítva, hogy a végtelen ciklus nem ér véget.

A generátorokkal nem kell aggódnunk a végtelen ciklus létrehozása miatt, mert a végrehajtást tetszés szerint megállíthatjuk és folytathatjuk. Azonban továbbra is óvatosnak kell lenned azzal, hogy hogyan hívod meg a generátort. Ha egy végtelen adatfolyamra spreadet vagy for...of-t használunk, akkor is egy végtelen cikluson fogunk egyszerre iterálni, ami a környezet összeomlásához vezet.

Egy bonyolultabb példát egy végtelen adatfolyamra, létrehozhatunk egy Fibonacci generátor függvényt. A Fibonacci-sorozatot, amely folyamatosan összeadja a két előző értéket, egy generátoron belüli végtelen ciklus segítségével írhatjuk meg a következőképpen:

Azért, hogy ezt kipróbáljuk, véges számon végighaladhatunk, és a Fibonacci-sorozatot kiírhatjuk a konzolra.

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

Az eredmény a következő lesz:

Output
0112358132134

A végtelen adathalmazokkal való munka képessége az egyik része annak, ami a generátorokat olyan hatékonnyá teszi. Ez olyan példáknál lehet hasznos, mint például a végtelen görgetés megvalósítása egy webes alkalmazás frontendjén.

Értékek átadása generátorokban

A cikk során a generátorokat iterátorként használtuk, és minden iterációban értékeket adtunk ki. A generátorok az értékek előállítása mellett értékeket is fogyaszthatnak next(). Ebben az esetben a yield tartalmazni fog egy értéket.

Fontos megjegyezni, hogy az első meghívott next() nem ad át értéket, hanem csak elindítja a generátort. Ennek demonstrálására naplózzuk a yield értékét, és néhányszor meghívjuk a next()-et néhány értékkel.

Ez a következő kimenetet adja:

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

A generátort egy kezdeti értékkel is be lehet vetni. A következő példában egy for hurkot készítünk, és minden egyes értéket átadunk a next() metódusba, de a kezdeti függvénynek is átadunk egy argumentumot:

A next()-ből lekérdezzük az értéket, és a következő iterációhoz egy új értéket adunk, ami az előző érték tízszerese. Így a következőt kapjuk:

Output
010203040

Egy másik módja a generátor indításának, ha a generátort egy olyan függvénybe csomagoljuk, amely mindig egyszer hívja meg a next()-t, mielőtt bármi mást csinálna.

async/await generátorokkal

Az aszinkron függvény az ES6+ JavaScriptben elérhető függvénytípus, amely az aszinkron adatokkal való munkát könnyebben érthetővé teszi azáltal, hogy szinkronnak tünteti fel. A generátorok a képességek szélesebb skálájával rendelkeznek, mint az aszinkron függvények, de hasonló viselkedést képesek reprodukálni. Az aszinkron programozás ilyen módon történő megvalósítása növelheti a kód rugalmasságát.

Ebben a szakaszban egy példát mutatunk a async/await generátorokkal történő reprodukálására.

Elkészítünk egy aszinkron függvényt, amely a Fetch API segítségével adatokat kér a JSONPlaceholder API-ból (amely tesztelési célokra JSON példadatokat biztosít), és a választ naplózza a konzolra.

Kezdjük egy getUsers nevű aszinkron függvény definiálásával, amely adatokat hív le az API-ból és egy objektumtömböt ad vissza, majd hívjuk meg a getUsers:

Ez a következőhöz hasonló JSON adatokat fog adni:

A generátorok használatával létrehozhatunk valami majdnem ugyanolyat, amely nem használja a async/await kulcsszavakat. Ehelyett egy általunk létrehozott új függvényt és yield értékeket fog használni a await ígéretek helyett.

A következő kódblokkban definiálunk egy getUsers nevű függvényt, amely a async/await utánzására az új asyncAlt függvényünket használja (amelyet később fogunk megírni).

Mint látjuk, ez majdnem ugyanúgy néz ki, mint a async/await implementáció, kivéve, hogy itt egy generátorfüggvényt adunk át, amely értékeket ad.

Most létrehozhatunk egy asyncAlt függvényt, amely hasonlít egy aszinkron függvényhez. A asyncAlt paramétere egy generátorfüggvény, amely a mi függvényünk, amely kiadja az ígéreteket, amelyeket a fetch visszaad. A asyncAlt maga is egy függvényt ad vissza, és minden egyes ígéretet felold, amit talál, egészen az utolsóig:

Ez ugyanazt a kimenetet adja, mint a async/await verzió:

Megjegyezzük, hogy ez az implementáció annak demonstrálására szolgál, hogyan használhatók a generátorok a async/await helyett, és nem egy gyártásra kész konstrukció. Nincs beállítva hibakezelés, és nem képes paraméterek átadására a kiadott értékekbe. Bár ez a módszer rugalmasabbá teheti a kódot, gyakran a async/await jobb választás lesz, mivel absztrahálja a megvalósítás részleteit, és lehetővé teszi, hogy a produktív kód írására összpontosítson.

Következtetés

A generátorok olyan folyamatok, amelyek képesek megállítani és folytatni a végrehajtást. Ezek a JavaScript hatékony, sokoldalú tulajdonságai, bár nem gyakran használják őket. Ebben a tananyagban megismerkedtünk a generátorfüggvényekkel és a generátorobjektumokkal, a generátorok számára elérhető módszerekkel, a yield és yield* operátorokkal, valamint a véges és végtelen adathalmazokkal használt generátorokkal. Megvizsgáltuk továbbá az aszinkron kód megvalósításának egyik módját egymásba ágyazott visszahívások vagy hosszú ígéretláncok nélkül.

Ha többet szeretne megtudni a JavaScript szintaxisáról, nézze meg a This, Bind, Call és Apply megértése JavaScriptben és a Map és Set objektumok megértése JavaScriptben című tananyagainkat.

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.