Ha még csak most kezdi a programozást, akkor nagy eséllyel a programokra, mint a logikai blokkok egy halmazára gondol, ahol minden blokk egy adott dolgot végez, és átadja annak eredményét, hogy a következő blokk futhasson és így tovább, és a nagyrészt igazad van, a legtöbb program szekvenciálisan fut, ez a modell lehetővé teszi számunkra, hogy egyszerűen írható és karbantartható programokat készítsünk. Vannak azonban speciális használati esetek, amikor ez a szekvenciális modell nem működik, vagy nem lenne optimális. Példaként vegyünk egy könyvolvasó alkalmazást. Ez az alkalmazás néhány speciális funkcióval rendelkezik, például megkeresi a szó összes előfordulását, navigál a könyvjelzők között és hasonlók. Most képzelje el, hogy a felhasználó éppen egy hosszú könyvet olvas, és úgy dönt, hogy megkeresi egy olyan gyakori szó összes előfordulását, mint például a „The”. Az alkalmazás általában néhány másodpercet vesz igénybe, hogy megtalálja és indexelje a szó összes előfordulását. Egy szekvenciális programban a felhasználó nem tud interakciót folytatni az alkalmazással (oldal megváltoztatása vagy szöveg kiemelése), amíg a keresési művelet be nem fejeződik. Remélhetőleg láthatja, hogy ez nem az optimális felhasználói élmény!
Az ábra a könyvolvasó alkalmazás tipikus végrehajtási folyamatát szemlélteti. Ha a felhasználó egy hosszan tartó műveletet kezdeményez (ebben az esetben egy nagy könyvben keresi a „the” szó összes előfordulását), az alkalmazás „lefagy” a művelet teljes időtartamára. Ebben az esetben a felhasználó a keresési művelet befejezéséig a következő könyvjelzőgombra kattint eredmény nélkül, és az összes művelet egyszerre érvénybe lép, így a végfelhasználó egy lemaradt alkalmazás érzését keltve.
Talán észrevette, hogy ez a példa nem igazán felel meg a korábban bemutatott szekvenciális modellnek. Ennek az az oka, hogy az itteni műveletek függetlenek egymástól. A felhasználónak nem kell tudnia a „the” előfordulások számáról, hogy a következő könyvjelzőre navigáljon, így a műveletek végrehajtási sorrendje nem igazán fontos. Nem kell megvárnunk a keresési művelet végét, mielőtt a következő könyvjelzőre navigálhatunk. Ezen a logikán alapul az előző végrehajtási folyamat egy lehetséges továbbfejlesztése: a háttérben futtathatjuk a hosszú keresési műveletet, folytathatjuk a bejövő műveleteket, és a hosszú művelet elvégzése után egyszerűen értesíthetjük a felhasználót. A végrehajtási folyamat a következőképpen alakul:
Ezzel a végrehajtási folyamattal a felhasználói élmény jelentősen javul. Mostantól a felhasználó kezdeményezhet egy hosszan tartó műveletet, folytathatja az alkalmazás szokásos használatát, és értesítést kaphat, ha a művelet befejeződött. Ez az aszinkron programozás alapja.
A Javascript, többek között, támogatja az aszinkron programozás ezen stílusát azáltal, hogy kiterjedt API-kat biztosít szinte bármilyen aszinkron viselkedés eléréséhez, amelyre csak gondolhat. A nap végén a Javascriptnek eredendően aszinkron nyelvnek kell lennie. Ha az előző példára hivatkozunk, az aszinkron logika az összes felhasználói interakciós alkalmazás alapja, a Javascript pedig elsősorban a böngészőben való használatra készült, ahol a legtöbb program a felhasználói műveletekre való reagálásról szól.
Az alábbiakban rövid útmutatót adunk az aszinkron Javascriptről:
Visszahívások
Egy tipikus programban általában számos funkciót talál. Egy függvény használatához egy paraméterkészlettel hívjuk meg. A függvénykód végrehajtódik, és eredményt ad vissza, semmi szokatlan. Az aszinkron programozás kissé eltolja ezt a logikát. Visszatérve a könyvolvasó alkalmazás példájára, nem használhatunk normál függvényt a keresési műveleti logika megvalósítására, mivel a művelet ismeretlen ideig tart. A normál függvény alapvetően a művelet elvégzése előtt tér vissza, és ez nem az a viselkedés, amelyet várunk. A megoldás az, hogy megadunk egy másik funkciót, amely a keresési művelet befejezése után kerül végrehajtásra. Ez modellezi a használati esetünket, mivel programunk a szokásos módon folytathatja lefutását, és a keresési művelet befejeztével a megadott funkció lefut, hogy értesítse a felhasználót a keresési eredményekről. Ezt a függvényt hívjuk visszahívási függvénynek:
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
Először definiáljuk a keresési művelet funkciót, a searchOccurrences. Szüksége van a keresendő szóra és egy második „visszahívás” paraméterre, amely a keresési művelet után végrehajtandó funkció lesz. A keresési művelet funkciót szándékosan absztraktnak tartottuk, csak a két lehetséges kimenetelére kell koncentrálnunk: az első esetben minden sikeres volt, és a keresés eredménye az eredményváltozóban szerepel. Ebben az esetben csak a visszahívási függvényt kell meghívnunk a következő paraméterekkel: az első paraméter null, ami azt jelenti, hogy nem történt hiba, a második paraméter a keresett szó, a harmadik és talán a legfontosabb paraméter a három közül., a keresési művelet eredménye.
A második eset az, amikor hiba történik, ez is olyan eset, amikor a keresési művelet végrehajtása megtörténik, és meg kell hívnunk a visszahívási függvényt. A try and catch blokkot használjuk a hiba elfogására, és csak a visszahívási függvényt hívjuk meg a catch blokk hibaobjektumával.
Ezután definiáltuk a handleSearchOccurrences visszahívási függvényt, a logikáját megőriztük egészen egyszerűnek. Csak egy üzenetet kell kinyomtatni a konzolra. Először ellenőrizzük az „err” paramétert, hogy lássuk, nem történt-e hiba a fő funkcióban. Ebben az esetben csak tudatjuk a felhasználóval, hogy a keresési művelet hibával ért véget. Ha nem jelentkezett hiba, kinyomtatjuk a keresési művelet eredményét.
Végül a searchOccurrences függvényt a „the” szóval hívjuk meg. A funkció ezentúl normálisan fog futni, a főprogram blokkolása nélkül, és ha a keresés megtörtént, a visszahívás végrehajtásra kerül, és az eredményüzenetet kapjuk a keresés eredményével vagy a hibaüzenettel.
Itt fontos megemlíteni, hogy csak a fő és a visszahívási függvényen belül férünk hozzá az eredmény változóhoz. Ha valami ilyesmivel próbálkozunk:
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
a nyomtatás eredménye definiálatlan lenne, mert a programok nem várják meg a searchOccurrences függvény végrehajtását. A következő utasításra lép, amely a print utasítás, mielőtt az eredményváltozó hozzárendelődik a fő függvényen belül. Ennek eredményeként kinyomtatjuk a hozzá nem rendelt eredményváltozót.
E logika alapján tehát az eredményváltozót használó összes kódot a visszahívási függvényen belül kell tartanunk. Lehet, hogy ez most nem tűnik problémának, de gyorsan valódi problémává fajulhat. Képzeljük el azt az esetet, amikor aszinkron függvények lánca van, amelyeknek sorban kell futniuk. A tipikus visszahívási logikában valami ilyesmit valósítana meg:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Ne feledje, hogy minden visszahívásnak van egy hibaparamétere, és minden hibát külön kell kezelni. Ez még bonyolultabbá és nehezen karbantarthatóbbá teszi a fenti, amúgy is összetett kódot. Remélhetőleg a Promises azért van itt, hogy megoldja a visszahívási pokol problémáját, a továbbiakban ezzel foglalkozunk.
Ígéretek
Az ígéretek a visszahívásokra épülnek, és hasonló módon működnek. Ezeket az ES6 funkcióinak részeként vezették be, hogy megoldjanak néhány szembetűnő problémát a visszahívásokkal, például a visszahívási pokollal. Az ígéretek saját funkciókat biztosítanak, amelyek sikeres befejezéskor (feloldás), és hiba esetén (elutasítás) futnak. Az alábbiakban a searchOccurrences példát mutatjuk be ígéretekkel:
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Nézzük át az általunk alkalmazott változtatásokat:
A searchOccurrences függvény ígéretet ad vissza. Az ígéreten belül ugyanazt a logikát követjük: két feloldó és elutasító funkciónk van, amelyek a visszahívásainkat képviselik, nem pedig egyetlen visszahívási függvény, amely mind a sikeres, mind a hibás végrehajtást kezeli. Az ígéretek elválasztják a két eredményt, és tiszta szintaxist biztosítanak a fő függvény meghívásakor. A feloldás funkció a fő funkcióhoz van „kötve” az „akkor” kulcsszó használatával. Itt csak megadjuk a feloldó függvény két paraméterét, és kinyomtatjuk a keresési eredményt. Hasonló dolog vonatkozik a reject funkcióra is, a „catch” kulcsszóval lehet rákötni. Remélhetőleg értékelni fogja az ígéretek által kínált előnyöket a kód olvashatósága és tisztasága tekintetében. Ha még mindig vitatkozik, nézze meg, hogyan oldhatjuk meg a visszahívási pokol problémáját az aszinkron függvények egymás utáni futtatásával:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Async/Await
Az Async/Await a Javascript aszinkron eszközsávjának legújabb kiegészítése. Az ES8-cal bevezetett új absztrakciós réteget biztosítanak az aszinkron funkciókon felül, egyszerűen „várnak” egy aszinkron művelet végrehajtására. A programfolyamat az adott utasításnál addig blokkol, amíg az aszinkron műveletből eredményt nem kap, majd a program a következő utasítással folytatja. Ha a szinkron végrehajtási folyamatra gondol, akkor igaza van. Megérkeztünk a teljes körhöz! Az Async/await kísérlet arra, hogy a szinkron programozás egyszerűségét az aszinkron világba hozza. Ne feledje, hogy ezt csak a program végrehajtása és kódja érzékeli. A motorháztető alatt minden marad a régiben, az Async/await továbbra is ígéreteket használ, és a visszahívások az építőköveik.
Nézzük át a példánkat, és valósítsuk meg az Async/await használatával:
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
A kódunk nem sokat változott, itt fontos észrevenni az „async” kulcsszót a searchOccurrences függvény deklarációja előtt. Ez azt jelzi, hogy a függvény aszinkron. Figyelje meg a „várni” kulcsszót is a searchOccurrences függvény hívásakor. Ez arra utasítja a programot, hogy várja meg a függvény végrehajtását az eredmény visszaadásáig, mielőtt a program a következő utasításra léphetne, más szóval az eredményváltozó mindig a searchOccurrences függvény visszaadott értékét fogja tartalmazni, nem pedig a a függvénynek ebben az értelemben az Async/Awaitnak nincs függő állapota ígéretként. A végrehajtás végeztével áttérünk a print utasításra, és ezúttal az eredmény valójában a keresési művelet eredményét tartalmazza. Ahogy az várható volt, az új kód ugyanúgy viselkedik, mintha szinkron lenne.
Egy másik apró dolog, amit szem előtt kell tartani, hogy mivel már nem rendelkezünk visszahívási függvényekkel, a searchOccurrences hibát ugyanazon a függvényen belül kell kezelnünk, mivel a hibát nem tudjuk csak úgy továbbítani a visszahívási függvényre, és ott kezelni. Itt csak a példa kedvéért hibaüzenetet nyomtatunk hiba esetén.
Befejezés
Ebben a cikkben áttekintettük az aszinkron logika Javascriptben való megvalósítására használt különböző megközelítéseket. Egy konkrét példával kezdtük, hogy miért kell áttérnünk a szokásos szinkron programozási stílusról az aszinkron modellre. Ezután áttértünk a visszahívásokra, amelyek az aszinkron Javascript fő építőkövei. A visszahívások korlátai elvezettek bennünket a különféle alternatívákhoz, amelyeket az évek során hozzáadtak a korlátok leküzdése érdekében, elsősorban az ígéretek és az Async/wait. Az aszinkron logika bárhol megtalálható a weben, legyen szó külső API-ról, adatbázislekérdezés kezdeményezéséről, a helyi fájlrendszerbe írásról vagy akár a bejelentkezési űrlapon a felhasználói bevitelre várásról. Remélhetőleg most már magabiztosabban kezelheti ezeket a problémákat, ha tiszta és karbantartható aszinkron Javascriptet ír!
Ha tetszik ez a cikk, kérjük, tekintse meg a CLA Blog oldalt, ahol különféle témákat tárgyalunk a technológiába való bejutásról. Nézze meg youtube csatornánkat korábbi ingyenes műhelyeinkért, és kövessen minket a közösségi médián, hogy ne maradj le az elkövetkezőkről!