Nézzük meg a JavaScript Async/Await funkcióját példákon keresztül
A cikk szerzője az Async/Await példáit vizsgálja JavaScriptben. Összességében az Async/Await kényelmes módja az aszinkron kód írásának. Mielőtt ez a funkció megjelent, az ilyen kódot visszahívások és ígéretek segítségével írták. Az eredeti cikk szerzője különféle példák elemzésével tárja fel az Async/Await előnyeit.
Emlékeztetünk:a "Habr" minden olvasója számára - 10 000 rubel kedvezmény, ha a "Habr" promóciós kóddal bármely Skillbox tanfolyamra jelentkezik.
A Skillbox a következőket ajánlja: Oktató online tanfolyam "Java fejlesztő".
Visszahívás
A visszahívás egy olyan funkció, amelynek hívása határozatlan ideig késik. Korábban a visszahívásokat azokon a kódterületeken alkalmazták, ahol nem lehetett azonnal megkapni az eredményt.
Íme egy példa a Node.js fájl aszinkron olvasására:
Problémák akkor merülnek fel, ha egyszerre több aszinkron műveletet kell végrehajtania. Képzeljük el ezt a forgatókönyvet: egy kérés érkezik az Arfat felhasználói adatbázishoz, el kell olvasnia a profile_img_url mezőjét, és letöltenie kell egy képet a someserver.com szerverről.
Letöltés után a képet más formátumba konvertáljuk, például PNG-ből JPEG-be. Ha az átalakítás sikeres volt, a rendszer egy levelet küld a felhasználó e-mail-címére. Ezután az eseményre vonatkozó információk bekerülnek a transformations.log fájlba, jelezve a dátumot.
Érdemes figyelni a visszahívások átfedésére és a kód utolsó részében található }) nagy számára. Úgy hívják, hogy Callback Hell vagy Piramis of Doom.
Ennek a módszernek a hátrányai nyilvánvalóak:
Ez a kód nehezen olvasható.
A hibák kezelése is nehézkes, ami gyakran rossz kódminőséghez vezet.
A probléma megoldására ígéreteket adtunk a JavaScripthez. Lehetővé teszik, hogy a visszahívások mély egymásba ágyazását a .the szóra cserélje.
Az ígéretek pozitívuma, hogy sokkal jobban olvashatóvá teszik a kódot, felülről lefelé, nem pedig balról jobbra. Az ígéreteknek azonban megvannak a maguk problémái:
Sok .akkor kell hozzá.
A try/catch helyett a .catch kezel minden hibát.
Egy hurkon belül több ígérettel dolgozni nem mindig kényelmes, bizonyos esetekben bonyolultabbá teszik a kódot.
Itt van egy probléma, amely megmutatja az utolsó pont jelentését.
Tegyük fel, hogy van egy for ciklusunk, amely 0-tól 10-ig terjedő számsorozatot ír ki véletlenszerű időközönként (0–n másodperc). Ígéreteket használva meg kell változtatni ezt a ciklust úgy, hogy a számok 0-tól 10-ig sorban jelenjenek meg. Tehát, ha egy nulla kinyomtatása 6 másodpercig, az egyes kinyomtatása 2 másodpercig tart, először a nullát kell kinyomtatni, majd azután megkezdődik a visszaszámlálás a kinyomtatáshoz.
És természetesen nem használjuk az Async/Await vagy a .sort parancsot a probléma megoldására. Egy példa megoldás a végén.
Aszinkron funkciók
Az ES2017 (ES8) aszinkron függvények hozzáadása leegyszerűsítette az ígéretekkel való munkát. Megjegyzem, hogy az aszinkron funkciók az ígéretek „felül” működnek. Ezek a függvények nem képviselnek minőségileg eltérő fogalmakat. Az aszinkron függvények az ígéreteket használó kód alternatívái.
Az Async/Await lehetővé teszi az aszinkron kóddal végzett munka szinkron stílusban történő szervezését.
Így az ígéretek ismerete megkönnyíti az Async/Await elveinek megértését.
szintaxis
Általában két kulcsszóból áll: async és await. Az első szó a függvényt aszinkronná változtatja. Az ilyen funkciók lehetővé teszik a várakozás használatát. Minden más esetben ennek a funkciónak a használata hibát generál.
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
Az Async a függvénydeklaráció legelejére, nyílfüggvény esetén pedig a „=” jel és a zárójel közé kerül.
Ezek a függvények elhelyezhetők egy objektumban metódusként, vagy használhatók osztálydeklarációban.
// As an object's method
const obj = {
async getName() {
return fetch('https://www.example.com');
}
}
// In a class
class Obj {
async getResource() {
return fetch('https://www.example.com');
}
}
Megjegyzés! Érdemes megjegyezni, hogy az osztálykonstruktorok és a getterek/beállítók nem lehetnek aszinkronok.
Szemantika és végrehajtási szabályok
Az aszinkron funkciók alapvetően hasonlóak a szabványos JS függvényekhez, de vannak kivételek.
Így az aszinkron függvények mindig ígéreteket adnak vissza:
async function fn() {
return 'hello';
}
fn().then(console.log)
// hello
Pontosabban, az fn a hello karakterláncot adja vissza. Nos, mivel ez egy aszinkron függvény, a karakterlánc értékét egy konstruktor segítségével egy ígéretbe csomagolják.
Íme egy alternatív kialakítás Async nélkül:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// hello
Ebben az esetben az ígéret „manuálisan” kerül visszaadásra. Az aszinkron függvény mindig új ígéretbe van csomagolva.
Ha a visszatérési érték primitív, az aszinkron függvény ígéretbe csomagolva adja vissza az értéket. Ha a visszatérési érték ígéret objektum, akkor annak felbontása új ígéretben kerül visszaadásra.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
De mi történik, ha hiba van egy aszinkron függvényben?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);
Ha nem kerül feldolgozásra, a foo() ígéretet ad vissza, elutasítással. Ebben a helyzetben a Promise.resolve helyett a hibát tartalmazó Promise.reject kerül visszaadásra.
Az aszinkron függvények mindig ígéretet adnak ki, függetlenül attól, hogy mit adunk vissza.
Az aszinkron funkciók minden várakozásnál szünetelnek.
Várakozás hatással kifejezések. Tehát, ha a kifejezés ígéret, akkor az aszinkron funkció felfüggesztésre kerül, amíg az ígéret teljesül. Ha a kifejezés nem ígéret, akkor a Promise.resolve segítségével ígéretté alakítja, majd befejezi.
// utility function to cause delay
// and get random value
const delayAndGetRandom = (ms) => {
return new Promise(resolve => setTimeout(
() => {
const val = Math.trunc(Math.random() * 100);
resolve(val);
}, ms
));
};
async function fn() {
const a = await 9;
const b = await delayAndGetRandom(1000);
const c = await 5;
await delayAndGetRandom(1000);
return a + b * c;
}
// Execute fn
fn().then(console.log);
És itt van egy leírás az fn függvény működéséről.
Meghívása után az első sor konvertálódik const a = await 9-ből; in const a = await Promise.resolve(9);.
A Await használata után a függvény végrehajtása felfüggesztésre kerül, amíg a meg nem kapja az értékét (jelen esetben ez 9).
A delayAndGetRandom(1000) szünetelteti az fn függvény végrehajtását, amíg az önmagától be nem fejeződik (1 másodperc elteltével). Ez gyakorlatilag leállítja az fn funkciót 1 másodpercre.
A delayAndGetRandom(1000) a felbontáson keresztül egy véletlenszerű értéket ad vissza, amelyet aztán a b változóhoz rendelünk.
Nos, a c változó esete hasonló az a változó esetéhez. Ezt követően minden leáll egy másodpercre, de most a delayAndGetRandom(1000) semmit sem ad vissza, mert nem kötelező.
Ennek eredményeként az értékeket az a + b * c képlet alapján számítják ki. Az eredményt a Promise.resolve használatával ígéretbe csomagolja, és a függvény visszaadja.
Ezek a szünetek emlékeztethetnek az ES6 generátoraira, de van benne valami az indokaidat.
A probléma megoldása
Nos, most nézzük a fent említett probléma megoldását.
A finishMyTask függvény a Await funkciót használja, hogy megvárja az olyan műveletek eredményét, mint a queryDatabase, sendEmail, logTaskInFile és mások. Ha összehasonlítja ezt a megoldást azzal, ahol az ígéreteket használták, nyilvánvalóvá válnak a hasonlóságok. Az Async/Await verzió azonban nagymértékben leegyszerűsíti az összes szintaktikai bonyolultságot. Ebben az esetben nincs nagy számú visszahívás és lánc, mint például a .then/.catch.
Itt van egy megoldás a számok kimenetével, két lehetőség van.
const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
let pr = Promise.resolve(0);
for (let i = 1; i <= 10; i += 1) {
pr = pr.then((val) => {
console.log(val);
return wait(i, Math.random() * 1000);
});
}
resolve(pr);
});
// Implementation Two (Using Recursion)
const printNumbersRecursive = () => {
return Promise.resolve(0).then(function processNextPromise(i) {
if (i === 10) {
return undefined;
}
return wait(i, Math.random() * 1000).then((val) => {
console.log(val);
return processNextPromise(i + 1);
});
});
};
És itt van egy megoldás az aszinkron függvények használatával.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}
Hiba a feldolgozásban
A kezeletlen hibák visszautasított ígéretbe vannak csomagolva. Az aszinkron funkciók azonban a try/catch használatával szinkronban kezelhetik a hibákat.
async function canRejectOrReturn() {
// wait one second
await new Promise(res => setTimeout(res, 1000));
// Reject with ~50% probability
if (Math.random() > 0.5) {
throw new Error('Sorry, number too big.')
}
return 'perfect number';
}
A canRejectOrReturn() egy aszinkron függvény, amely vagy sikeres ("tökéletes szám"), vagy hibával meghiúsul ("Sajnáljuk, a szám túl nagy").
Mivel a fenti példa a canRejectOrReturn végrehajtását várja, saját hibája a catch blokk végrehajtását fogja eredményezni. Ennek eredményeként a foo függvény vagy undefined-re (amikor semmit sem ad vissza a try blokkban) vagy elkapott hibával végződik. Ennek eredményeként ez a funkció nem fog meghibásodni, mert a try/catch magát a foo függvényt fogja kezelni.
Érdemes figyelni arra, hogy a példában a canRejectOrReturn visszaadása a foo-tól. A Foo ebben az esetben vagy tökéletes számmal fejeződik be, vagy hibát ad vissza („Elnézést, a szám túl nagy”). A fogási blokk soha nem kerül végrehajtásra.
A probléma az, hogy a foo visszaadja a canRejectOrReturn által adott ígéretet. Így a foo megoldása a canRejectOrReturn megoldásává válik. Ebben az esetben a kód csak két sorból áll:
A fenti kódban a foo sikeresen kilép mind tökéletes számmal, mind elkapott hibával. Itt nem lesz elutasítás. De a foo a canRejectOrReturn-nel fog visszatérni, nem az undefined-el. Győződjön meg erről a return await canRejectOrReturn() sor eltávolításával:
Mint látható, a kódban nincs várakozás vagy visszatérés. Ezért a foo mindig undefined-el lép ki 1 másodperces késleltetés nélkül. De az ígéret teljesülni fog. Ha hibát vagy elutasítást ad, akkor az UnhandledPromiseRejectionWarning meghívásra kerül.
Aszinkron funkciók a visszahívásokban
Az aszinkron függvényeket gyakran használják visszahívásként a .map vagy .filter fájlokban. Példa erre a fetchPublicReposCount(username) függvény, amely visszaadja a GitHubon nyitott tárhelyek számát. Tegyük fel, hogy van három felhasználó, akiknek a mérőszámaira szükségünk van. Íme a kód ehhez a feladathoz:
A .map visszahívásnál érdemes odafigyelni a Várakozásra. Itt a counts az ígéretek tömbje, a .map pedig egy névtelen visszahívás minden megadott felhasználó számára.
A várakozás túl következetes használata
Vegyük ezt a kódot példaként:
async function fetchAllCounts(users) {
const counts = [];
for (let i = 0; i < users.length; i++) {
const username = users[i];
const count = await fetchPublicReposCount(username);
counts.push(count);
}
return counts;
}
Itt a repo szám a count változóba kerül, majd ez a szám hozzáadódik a counts tömbhöz. A kóddal az a probléma, hogy amíg az első felhasználó adatai meg nem érkeznek a szerverről, addig minden további felhasználó készenléti üzemmódban lesz. Így egyszerre csak egy felhasználó kerül feldolgozásra.
Ha például egy felhasználó feldolgozása körülbelül 300 ms-t vesz igénybe, akkor az összes felhasználónál már egy másodperc, az eltöltött idő lineárisan függ a felhasználók számától. De mivel a repo számának megszerzése nem függ egymástól, a folyamatok párhuzamosíthatók. Ehhez a .map és a Promise.all használata szükséges:
A Promise.all egy sor ígéretet kap bemenetként, és egy ígéretet ad vissza. Ez utóbbi, miután a tömbben lévő összes ígéret teljesült, vagy az első elutasításkor, teljesül. Előfordulhat, hogy nem egyszerre indul el – az egyidejű indítás érdekében használhatja a p-map-et.
Következtetés
Az aszinkron funkciók egyre fontosabbak a fejlesztés szempontjából. Nos, az aszinkron funkciók adaptív használatához érdemes használni Aszinkron iterátorok. Egy JavaScript fejlesztőnek ebben jártasnak kell lennie.