- Egy gyors bevezető
- Generátorfüggvények JavaScriptben
- Mi az a generátorfüggvény?
- Az első generátorfüggvényünk
- Iterábilisok és iterátorok
- Az iterátorok terjeszthetők és destrukturálhatók
- Adatok kinyerése egy generátorfüggvényből
- Visszatérve a generátorfüggvényekhez
- A generátorfüggvények felhasználási esetei
- Aszinkron generátorfüggvények JavaScriptben
- Mi az az aszinkron generátorfüggvény?
- Az első aszinkron generátorfüggvényünk
- Aszinkron iterábilisok és iterátorok
- Adatok kinyerése egy aszinkron generátorból
- Visszatérve az aszinkron generátorfüggvényekhez
- Az aszinkron iterátorok és aszinkron generátorfüggvények felhasználási esetei
- Összegzés
Egy gyors bevezető
Köszöntöm önöket a generátorfüggvények és aszinkron generátorok csodálatos világában JavaScriptben.
Az egyik legegzotikusabb (a fejlesztők többsége szerint) JavaScript funkcióval fog megismerkedni.
Lássunk hozzá!
Generátorfüggvények JavaScriptben
Mi az a generátorfüggvény?
A generátorfüggvény (ECMAScript 2015) JavaScriptben a szinkron függvények egy speciális típusa, amely képes tetszés szerint leállítani és folytatni a végrehajtását.
A hagyományos JavaScript függvényekkel ellentétben, amelyek tűz és felejtés, a generátorfüggvények képesek arra is, hogy:
- kétirányú csatornán keresztül kommunikáljanak a hívóval.
- megőrzik a végrehajtási kontextusukat (hatókörüket) a későbbi hívások során.
A generátorfüggvényekre úgy is gondolhatunk, mint a zárlatokra szteroidokon, de a hasonlóságok itt véget érnek!
Az első generátorfüggvényünk
A generátorfüggvény létrehozásához a function
kulcsszó után egy csillagot *
teszünk:
function* generate() {//}
Figyelem: a generátorfüggvények felvehetik egy osztálymódszer vagy egy függvény kifejezés alakját is. Ezzel szemben a nyíl generátorfüggvények nem megengedettek.
A függvényen belül a yield
segítségével szüneteltethetjük a végrehajtást:
function* generate() { yield 33; yield 99;}
yield
szünetelteti a végrehajtást, és egy úgynevezett Generator
objektumot ad vissza a hívónak. Ez az objektum egyszerre iterábilis és iterátor is.
Elmisztifikáljuk ezeket a fogalmakat.
Iterábilisok és iterátorok
Az iterábilis objektum a JavaScriptben egy Symbol.iterator
implementáló objektum. Íme egy minimális példa az iterábilisra:
const iterable = { : function() { /* TODO: iterator */ }};
Mihelyt megvan ez az iterábilis objektum, hozzárendelünk egy függvényt a -hez, hogy visszaadjon egy iterátorobjektumot.
Ez sok elméletnek hangzik, de a gyakorlatban az iterábilis egy olyan objektum, amin a for...of
(ECMAScript 2015) segítségével loopolhatunk. Erről bővebben egy perc múlva.
Egy pár iterálhatót már ismerned kell a JavaScriptben: a tömbök és a karakterláncok például iterálhatóak:
for (const char of "hello") { console.log(char);}
Egy másik ismert iterálható a Map
és a Set
. A for...of
szintén jól jön egy objektum értékei feletti iterációhoz:
const person = { name: "Juliana", surname: "Crain", age: 32};for (const value of Object.values(person)) { console.log(value);}
Azt ne feledjük, hogy a enumerable: false
-vel jelölt tulajdonságok nem fognak megjelenni az iterációban:
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
Az a probléma az egyéni iterábilisunkkal, hogy iterátor nélkül egyedül nem jut messzire.
Az iterátorok is objektumok, de meg kell felelniük az iterátor protokollnak. Röviden, az iterátoroknak legalább egy next()
metódussal kell rendelkezniük.
next()
Egy másik objektumot kell visszaadnia, amelynek tulajdonságai value
és done
.
A next()
metódusunk logikájának a következő szabályoknak kell megfelelnie:
- az iteráció folytatásához egy
done: false
objektumot adunk vissza. - az iteráció leállításához egy
done: true
objektumot adunk vissza.
value
helyette azt az eredményt kell tartanunk, amit a fogyasztó számára szeretnénk előállítani.
Bővítsük példánkat az iterátorral:
const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};
Itt van egy iterable, ami helyesen valósítja meg a Symbol.iterator
-t. Van egy iterátorunk is, amely visszaad:
- egy olyan objektumot, amelynek alakja
{ value: x, done: false}
, amígcount
el nem éri a 3-at. - egy olyan objektumot, amelynek alakja
{ value: x, done: true}
, amikorcount
eléri a 3-at.
Ezt a minimális iterálhatót készen áll arra, hogy for...of
segítségével iteráljuk:
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);}
Az eredmény:
123
Amint jobban megismerjük a generátorfüggvényeket, látni fogjuk, hogy az iterátorok a generátorobjektumok alapja.
Ne feledd:
- az iterálható objektumok azok, amelyeken iterálunk.
- Az iterátorok azok a dolgok, amelyek az iterálhatót … “átkattinthatóvá” teszik.
A végén, mi értelme van az iterálhatónak?
Már van egy szabványos, megbízható for...of
ciklusunk, amely gyakorlatilag szinte bármilyen adatstruktúrára működik, legyen az egyéni vagy natív, JavaScriptben.
Az for...of
használatához az egyéni adatstruktúrádon a következőket kell tenned:
- implementáld
Symbol.iterator
. - adj egy iterátor objektumot.
Ez minden!
További források:
- Iterators gonna iterate by Jake Archibald.
Az iterátorok terjeszthetők és destrukturálhatók
A for...of
mellett véges iterátorokon is használhatjuk a terítést és a destrukturálást.
Mondjuk újra az előző példát:
const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};
Az összes érték kihúzásához az iterábilis szétteríthető egy tömbbe:
const values = ;console.log(values); //
Hogy ehelyett csak néhány értéket húzzunk ki, az iterábilis tömbös destruktúrálható. Itt az első és a második értéket kapjuk ki az iterábilisból:
const = iterable;console.log(first); // 1console.log(second); // 2
Ez helyett az első, és a harmadik értéket kapjuk ki:
const = iterable;console.log(first); // 1console.log(third); // 3
Fókuszunk most ismét a generátorfüggvényekre fordul.
Adatok kinyerése egy generátorfüggvényből
Ha már megvan egy generátorfüggvény, akkor elkezdhetünk vele interakcióba lépni. Ez az interakció abból áll, hogy:
- lépésről lépésre értékeket veszünk ki a generátorból.
- választhatóan értékeket küldünk vissza a generátorba.
A generátorból való értékkivonáshoz háromféle megközelítést használhatunk:
- az
next()
iterátor objektum meghívása. - iterálás
for...of
segítségével. - szórás és tömbök destrukturálása.
A generátorfüggvény nem számítja ki az összes eredményét egyetlen lépésben, mint a hagyományos függvények.
Ha példánkat vesszük, ahhoz, hogy a generátorból értékeket kapjunk, először is bemelegíthetjük a generátort:
function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();
Itt go
lesz az iterálható/iterátor objektumunk, a generate
hívásának eredménye.
(Ne feledjük, egy Generator
objektum egyszerre iterábilis és iterátor).
Mostantól kezdve hívhatjuk a go.next()
-t, hogy továbblépjünk a végrehajtásban:
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
Itt minden go.next()
hívás egy új objektumot eredményez. A példában ebből az objektumból destrukturáljuk a value
tulajdonságot.
Az iterátor objektum next()
hívásából visszaadott objektumok két tulajdonsággal rendelkeznek:
-
value
: az aktuális lépés értéke. -
done
: egy boolean, amely jelzi, hogy van-e még több érték a generátorban, vagy nincs.
Egy ilyen iterátor objektumot az előző részben implementáltunk. Ha generátort használunk, az iterátor objektum már ott van a számunkra.
next()
jól működik véges adatok kinyerésére egy iterátor objektumból.
Nem véges adatokon való iteráláshoz használhatjuk a for...of
. Íme egy végtelen generátor:
function* endlessGenerator() { let counter = 0; while (true) { counter++; yield counter; }}// Consume the generatorfor (const value of endlessGenerator()) { console.log(value);}
Mint láthatjuk, a for...of
használatakor nincs szükség a generátor inicializálására.
Végül a generátor objektumot is szétoszthatjuk és destruktúrálhatjuk. Itt van a szórás:
function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Spreadconst values = ;console.log(values); //
Itt a destrukturálás:
function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Destructuringconst = go;console.log(first); // 1console.log(second); // 2
Fontos megjegyezni, hogy a generátorok kimerülnek, ha minden értéküket elfogyasztjuk.
Ha egy generátorból értékeket szórsz ki, utána már nincs mit kihúzni:
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
A generátorok fordítva is működnek: a hívótól értékeket vagy parancsokat fogadhatnak el, ahogy azt mindjárt látni fogjuk.
Visszatérve a generátorfüggvényekhez
A generátorobjektumok, a generátorfüggvény hívásának eredményül kapott objektumok a következő metódusokat tárják fel:
next()
return()
throw()
Az next()
metódust már láttuk, amely a generátorból való objektumok kihúzásában segít.
Nem csak az adatok kinyerésére korlátozódik a használata, hanem arra is, hogy értékeket küldjünk a generátornak.
Nézzük a következő generátort:
function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}
Ez egy string
paramétert sorol fel. Ezt az argumentumot az inicializáláskor adjuk meg:
function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");
Mihelyt az next()
-t először hívjuk az iterátor objektumon, a végrehajtás elindul, és “A”-t eredményez:
function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // A
Ezzel a ponttal visszaszólhatunk a generátornak, ha megadjuk a next
argumentumát:
go.next("b");
Itt a teljes lista:
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
Mostantól kezdve bármikor betáplálhatunk értékeket a yield
-be, ha új nagybetűs karakterláncra van szükségünk:
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
Ha bármikor teljesen vissza akarunk térni a végrehajtásból, használhatjuk az return
-t az iterátor objektumon:
const { value } = go.return("stop it");console.log(value); // stop it
Ez megállítja a végrehajtást.
Az értékek mellett kivételt is dobhatunk a generátorba. Lásd: “Hibakezelés a generátorfüggvényekhez”.
A generátorfüggvények felhasználási esetei
A legtöbb fejlesztő (köztük én is) a generátorfüggvényeket egy egzotikus JavaScript funkciónak tekinti, amelynek a való világban alig vagy egyáltalán nincs alkalmazása.
Ez igaz lehet az átlagos front-end munkára, ahol egy kis jQuery és egy kis CSS a legtöbbször elég a trükközéshez.
A valóságban a generátorfüggvények mindazokban a forgatókönyvekben ragyognak igazán, ahol a teljesítmény a legfontosabb.
Különösen jól használhatóak:
- nagyméretű fájlokkal és adathalmazokkal való munkához.
- adatfeldolgozás a back-endben vagy a front-endben.
- végtelen adatsorok generálásához.
- drága logika igény szerinti kiszámításához.
A generátorfüggvények a kifinomult aszinkron minták építőkövei is az aszinkron generátorfüggvényekkel, a következő szakaszunk témája.
Aszinkron generátorfüggvények JavaScriptben
Mi az az aszinkron generátorfüggvény?
Az aszinkron generátorfüggvény (ECMAScript 2018) az aszinkron függvények egy speciális típusa, amely képes tetszés szerint leállítani és folytatni a végrehajtását.
A különbség a szinkron generátorfüggvények és az aszinkron generátorfüggvények között az, hogy az utóbbiak aszinkron, Promise-alapú eredményt adnak vissza az iterátorobjektumból.
A generátorfüggvényekhez hasonlóan az aszinkron generátorfüggvények is képesek:
- kommunikálni a hívóval.
- megőrizni a végrehajtási kontextusukat (scope) a későbbi hívások során.
Az első aszinkron generátorfüggvényünk
Aszinkron generátorfüggvény létrehozásához deklarálunk egy generátorfüggvényt a *
csillaggal, async
előtaggal:
async function* asyncGenerator() { //}
A függvényen belül a yield
segítségével szüneteltethetjük a végrehajtást:
async function* asyncGenerator() { yield 33; yield 99;}
Itt a yield
szünetelteti a végrehajtást és egy úgynevezett Generator
objektumot ad vissza a hívónak.
Ez az objektum egyszerre iterábilis, és iterátor is.
Foglaljuk össze ezeket a fogalmakat, hogy lássuk, hogyan illeszkednek az aszinkron földre.
Aszinkron iterábilisok és iterátorok
Az aszinkron iterábilis a JavaScriptben egy Symbol.asyncIterator
megvalósító objektum.
Íme egy minimális példa:
const asyncIterable = { : function() { /* TODO: iterator */ }};
Ha már megvan ez az iterálható objektum, akkor hozzárendelünk egy függvényt a -hoz, hogy visszaadjon egy iterátor objektumot.
Az iterátor objektumnak meg kell felelnie az iterátor protokollnak egy next()
metódussal (mint a szinkron iterátornak).
Bővítsük példánkat az iterátorral:
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 }); } }; }};
Ez az iterátor hasonló ahhoz, amit az előző részekben építettünk, ezúttal csak annyi a különbség, hogy a visszatérő objektumot Promise.resolve
-vel csomagoljuk.
Ezzel a ponttal valami hasonlót tehetünk:
const go = asyncIterable();go.next().then(iterator => console.log(iterator.value));go.next().then(iterator => console.log(iterator.value));// 1// 2
Vagy a for await...of
segítségével:
async function consumer() { for await (const asyncIterableElement of asyncIterable) { console.log(asyncIterableElement); }}consumer();// 1// 2// 3
Az aszinkron iterátorok és iterátorok az aszinkron generátorfüggvények alapja.
Közelítsünk most ismét rájuk.
Adatok kinyerése egy aszinkron generátorból
Az aszinkron generátorfüggvények nem számítják ki az összes eredményüket egyetlen lépésben, mint a hagyományos függvények.
Ehelyett lépésenként húzunk ki értékeket.
Az aszinkron iterátorok és iterábilisok vizsgálata után nem lehet meglepő, hogy az aszinkron generátorok Promise-ok kihúzásához kétféle megközelítést használhatunk:
- Az iterátorobjektumon
next()
meghívása. - aszinkron iteráció a
for await...of
-vel.
Az eredeti példánkban ezt tehetjük:
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));
A kód kimenete:
3399
A másik megközelítés aszinkron iterációt használja a for await...of
-vel. Az aszinkron iteráció használatához a fogyasztót egy async
függvénnyel csomagoljuk be.
Itt a teljes példa:
async function* asyncGenerator() { yield 33; yield 99;}async function consumer() { for await (const value of asyncGenerator()) { console.log(value); }}consumer();
for await...of
szépen működik a nem véges adatfolyamok kinyerésére.
Lássuk most, hogyan küldjük vissza az adatokat a generátornak.
Visszatérve az aszinkron generátorfüggvényekhez
Gondoljunk a következő aszinkron generátorra:
async function* asyncGenerator(string) { while (true) { string = yield string.toUpperCase(); }}
A végtelen nagybetűs géphez hasonlóan a generátor példából a next()
-nek is megadhatunk egy argumentumot:
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();
Itt minden lépés egy új értéket küld a generátorba.
A kód kimenete:
ABC
Még ha nincs is semmi eredendően aszinkron a toUpperCase()
-ban, érzékelhető ennek a mintának a célja.
Ha bármikor ki akarunk lépni a végrehajtásból, hívhatjuk az return()
-t az iterátor objektumon:
const { value } = await go.return("stop it"); console.log(value); // stop it
Az értékek mellett kivételt is dobhatunk a generátorba. Lásd: “Hibakezelés aszinkron generátorokhoz”.
Az aszinkron iterátorok és aszinkron generátorfüggvények felhasználási esetei
Ha a generátorfüggvények arra jók, hogy nagy fájlokkal és végtelen sorozatokkal szinkronban dolgozzunk, az aszinkron generátorfüggvények a lehetőségek teljesen új földjét teszik lehetővé a JavaScript számára.
Különösen az aszinkron iteráció megkönnyíti az olvasható adatfolyamok fogyasztását. A Response
objektum a Fetchben body
olvasható folyamként tárja fel getReader()
. Egy ilyen folyamot becsomagolhatunk egy aszinkron generátorral, és később a for await...of
segítségével iterálhatunk rajta.
A Jake Archibald által írt Async iterators and generators tartalmaz egy csomó szép példát.
További példák a streamekre a request streamek a Fetch-el.
Az írás idején még nincs böngésző API, ami implementálná a Symbol.asyncIterator
-t, de a Stream spec ezt meg fogja változtatni.
A Node.js a legújabb Stream API szépen játszik az aszinkron generátorokkal és az aszinkron iterációval.
A jövőben képesek leszünk írható, olvasható és transzformálható streameket fogyasztani és zökkenőmentesen dolgozni velük az ügyféloldalon.
További források:
- A folyam API.
Összegzés
Az ebben a bejegyzésben tárgyalt legfontosabb kifejezések és fogalmak:
ECMAScript 2015:
- iterable.
- iterátor.
Ezek a generátorfüggvények építőkövei.
ECMAScript 2018:
- aszinkron iterábilis.
- aszinkron iterátor.
Ezek helyett az aszinkron generátorfüggvények építőkövei.
Az iteráblik és az iterátorok jó ismerete messzire vezethet. Nem arról van szó, hogy minden nap generátorfüggvényekkel és aszinkron generátorfüggvényekkel fogsz dolgozni, de ezek jó képességek az eszköztáradban.
És te? Használtad már őket?
Köszönjük az olvasást, és maradj velünk a blogon!