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:

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

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").

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

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.

Íme egy másik példa:

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

É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:

try {
    const promise = canRejectOrReturn();
    return promise;
}

Íme, mi történik, ha együtt használja a várakozás és visszatérés funkciót:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

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:

try {
    const value = await canRejectOrReturn();
    return value;
}
// …

Gyakori hibák és buktatók

Egyes esetekben az Async/Await használata hibákhoz vezethet.

Elfelejtett vár

Ez elég gyakran megtörténik - a várakozás kulcsszót elfelejtik az ígéret előtt:

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}

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:

const url = 'https://api.github.com/users';
 
// Utility fn to fetch repo counts
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

ArfatSalman, octocat, norvig fiókok kellenek. Ebben az esetben a következőket tesszük:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];
 
const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

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:

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}

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.

A Skillbox a következőket ajánlja:

Forrás: will.com

Hozzászólás