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:
OutputgeneratorFunction {<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:
OutputNeoMorpheusTrinity
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:
Output1234
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:
Output0112358132134
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:
Output100200{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:
Output010203040
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.