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íg count el nem éri a 3-at.
  • egy olyan objektumot, amelynek alakja { value: x, done: true}, amikor count 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!

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

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