Pozrime sa na Async/Await v JavaScripte pomocou príkladov

Autor článku skúma príklady Async/Await v JavaScripte. Celkovo je Async/Await pohodlný spôsob písania asynchrónneho kódu. Predtým, ako sa táto funkcia objavila, bol takýto kód napísaný pomocou spätných volaní a prísľubov. Autor pôvodného článku odhaľuje výhody Async/Await analýzou rôznych príkladov.

Pripomíname vám: pre všetkých čitateľov „Habr“ - zľava 10 000 rubľov pri registrácii do akéhokoľvek kurzu Skillbox pomocou propagačného kódu „Habr“.

Skillbox odporúča: Vzdelávací online kurz "Java developer".

Spätné volanie

Spätné volanie je funkcia, ktorej volanie je odložené na neurčito. Predtým sa spätné volania používali v tých oblastiach kódu, kde nebolo možné okamžite získať výsledok.

Tu je príklad asynchrónneho čítania súboru v Node.js:

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

Problémy vznikajú, keď potrebujete vykonať niekoľko asynchrónnych operácií naraz. Predstavme si tento scenár: odošle sa požiadavka do databázy používateľov Arfat, musíte si prečítať jej pole profile_img_url a stiahnuť obrázok zo servera someserver.com.
Po stiahnutí obrázok prevedieme do iného formátu, napríklad z PNG do JPEG. Ak bola konverzia úspešná, na e-mail používateľa sa odošle list. Ďalej sa do súboru transforms.log zadajú informácie o udalosti s uvedením dátumu.

Stojí za to venovať pozornosť prekrývaniu spätných volaní a veľkému počtu }) v záverečnej časti kódu. Volá sa Callback Hell alebo Pyramid of Doom.

Nevýhody tejto metódy sú zrejmé:

  • Tento kód je ťažko čitateľný.
  • Je tiež ťažké zvládnuť chyby, čo často vedie k nízkej kvalite kódu.

Na vyriešenie tohto problému boli do JavaScriptu pridané sľuby. Umožňujú nahradiť hlboké vnorenie spätných volaní slovom .potom.

Pozitívnym aspektom prísľubov je, že kód je vďaka nim oveľa lepšie čitateľný zhora nadol a nie zľava doprava. Sľuby však majú aj svoje problémy:

  • Musíte pridať veľa .potom.
  • Namiesto try/catch sa na spracovanie všetkých chýb používa .catch.
  • Práca s viacerými prísľubmi v rámci jednej slučky nie je vždy pohodlná, v niektorých prípadoch komplikujú kód.

Tu je problém, ktorý ukáže význam posledného bodu.

Predpokladajme, že máme cyklus for, ktorý tlačí sekvenciu čísel od 0 do 10 v náhodných intervaloch (0–n sekúnd). Pomocou prísľubov musíte zmeniť túto slučku tak, aby sa čísla tlačili v poradí od 0 do 10. Ak teda tlač nuly trvá 6 sekúnd a tlač jednotky 2 sekundy, najskôr by sa mala vytlačiť nula a potom začne odpočítavanie pre tlač.

A samozrejme, na vyriešenie tohto problému nepoužívame Async/Await alebo .sort. Príklad riešenia je na konci.

Async funkcie

Pridanie asynchrónnych funkcií v ES2017 (ES8) zjednodušilo prácu so sľubmi. Všimol som si, že asynchrónne funkcie fungujú „nad“ sľubmi. Tieto funkcie nepredstavujú kvalitatívne odlišné pojmy. Async funkcie sú určené ako alternatíva ku kódu, ktorý používa sľuby.

Async/Await umožňuje organizovať prácu s asynchrónnym kódom v synchrónnom štýle.

Poznanie sľubov teda uľahčuje pochopenie princípov Async/Await.

syntax

Normálne pozostáva z dvoch kľúčových slov: async a wait. Prvé slovo zmení funkciu na asynchrónnu. Takéto funkcie umožňujú použitie čakania. V každom inom prípade použitie tejto funkcie vygeneruje chybu.

// With function declaration
 
async function myFn() {
  // await ...
}
 
// With arrow function
 
const myFn = async () => {
  // await ...
}
 
function myFn() {
  // await fn(); (Syntax Error since no async)
}
 

Async sa vkladá na úplný začiatok deklarácie funkcie a v prípade funkcie so šípkou medzi znak „=“ a zátvorky.

Tieto funkcie môžu byť umiestnené v objekte ako metódy alebo použité v deklarácii triedy.

// 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');
  }
}

NB! Stojí za to pripomenúť, že konštruktory tried a získavače/nastavovače nemôžu byť asynchrónne.

Sémantika a pravidlá vykonávania

Async funkcie sú v podstate podobné štandardným funkciám JS, existujú však výnimky.

Asynchrónne funkcie teda vždy vracajú sľuby:

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello

Konkrétne fn vráti reťazec ahoj. Keďže ide o asynchrónnu funkciu, hodnota reťazca je zabalená do prísľubu pomocou konštruktora.

Tu je alternatívny dizajn bez async:

function fn() {
  return Promise.resolve('hello');
}
 
fn().then(console.log);
// hello

V tomto prípade je prísľub vrátený „ručne“. Asynchrónna funkcia je vždy zabalená do nového prísľubu.

Ak je návratová hodnota primitívna, funkcia async vráti hodnotu tak, že ju zabalí do prísľubu. Ak je vrátená hodnota predmetom prísľubu, jej rozlíšenie sa vráti v novom prísľube.

const p = Promise.resolve('hello')
p instanceof Promise;
// true
 
Promise.resolve(p) === p;
// true
 

Čo sa však stane, ak sa v asynchrónnej funkcii vyskytne chyba?

async function foo() {
  throw Error('bar');
}
 
foo().catch(console.log);

Ak sa nespracuje, foo() vráti prísľub s odmietnutím. V tejto situácii sa namiesto Promise.resolve vráti Promise.reject obsahujúci chybu.

Asynchrónne funkcie vždy vydávajú prísľub bez ohľadu na to, čo sa vráti.

Asynchrónne funkcie sa pozastavia pri každom čakaní.

Čakať ovplyvňuje výrazy. Ak je teda výraz sľub, asynchronná funkcia je pozastavená, kým sa sľub nesplní. Ak výraz nie je prísľubom, prevedie sa na sľub cez Promise.resolve a potom sa dokončí.

// 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);

A tu je popis fungovania funkcie fn.

  • Po jeho zavolaní sa prvý riadok skonvertuje z const a = wait 9; in const a = wait Promise.resolve(9);.
  • Po použití Await sa vykonávanie funkcie pozastaví, kým a nezíska svoju hodnotu (v súčasnej situácii je to 9).
  • delayAndGetRandom(1000) pozastaví vykonávanie funkcie fn, kým sa nedokončí samo (po 1 sekunde). Tým sa efektívne zastaví funkcia fn na 1 sekundu.
  • delayAndGetRandom(1000) prostredníctvom riešenia vráti náhodnú hodnotu, ktorá sa potom priradí premennej b.
  • No, prípad s premennou c je podobný ako s premennou a. Potom sa všetko na sekundu zastaví, ale teraz delayAndGetRandom(1000) nevracia nič, pretože to nie je potrebné.
  • V dôsledku toho sa hodnoty vypočítajú pomocou vzorca a + b * c. Výsledok je zabalený do prísľubu pomocou Promise.resolve a vrátený funkciou.

Tieto pauzy môžu pripomínať generátory v ES6, ale niečo na tom je tvoje dôvody.

Riešenie problému

No a teraz sa pozrime na riešenie vyššie spomínaného problému.

Funkcia finishMyTask používa Await na čakanie na výsledky operácií ako queryDatabase, sendEmail, logTaskInFile a iné. Ak porovnáte toto riešenie s riešením, kde boli použité sľuby, podobnosti budú zrejmé. Verzia Async/Await však výrazne zjednodušuje všetky syntaktické komplikácie. V tomto prípade nedochádza k veľkému počtu spätných volaní a reťazí ako .then/.catch.

Tu je riešenie s výstupom čísel, sú dve možnosti.

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);
    });
  });
};

A tu je riešenie využívajúce asynchrónne funkcie.

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

Chyba pri spracovaní

Neošetrené chyby sú zabalené do odmietnutého sľubu. Asynchrónne funkcie však môžu používať try/catch na synchrónne spracovanie chýb.

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';
}

canRejectOrReturn() je asynchrónna funkcia, ktorá buď uspeje („dokonalé číslo“), alebo zlyhá s chybou („Prepáčte, príliš veľké číslo“).

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

Keďže vyššie uvedený príklad očakáva vykonanie canRejectOrReturn, jeho vlastné zlyhanie bude mať za následok vykonanie bloku catch. Výsledkom je, že funkcia foo skončí buď s nedefinovanou hodnotou (keď sa v bloku try nič nevráti), alebo so zachytenou chybou. V dôsledku toho táto funkcia nezlyhá, pretože try/catch zvládne funkciu foo sám.

Tu je ďalší príklad:

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

Stojí za to venovať pozornosť skutočnosti, že v príklade sa canRejectOrReturn vráti z foo. Foo v tomto prípade buď skončí s dokonalým číslom, alebo vráti chybu („Prepáčte, príliš veľké číslo“). Blok catch sa nikdy nevykoná.

Problém je v tom, že foo vráti sľub odovzdaný z canRejectOrReturn. Takže riešenie foo sa stáva riešením canRejectOrReturn. V tomto prípade bude kód pozostávať iba z dvoch riadkov:

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

Čo sa stane, ak použijete wait a return spolu:

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

Vo vyššie uvedenom kóde sa foo úspešne ukončí s dokonalým číslom a zachytenou chybou. Nebudú tu žiadne odmietnutia. Ale foo sa vráti s canRejectOrReturn, nie s undefined. Presvedčíme sa o tom odstránením riadku return wait canRejectOrReturn():

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

Časté chyby a úskalia

V niektorých prípadoch môže použitie funkcie Async/Await viesť k chybám.

Zabudnuté čakanie

Stáva sa to pomerne často – kľúčové slovo wait je zabudnuté pred prísľubom:

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

Ako vidíte, v kóde nie je žiadne čakanie ani návrat. Preto foo vždy opustí nedefinované bez 1 sekundového oneskorenia. Ale sľub sa splní. Ak vyvolá chybu alebo odmietnutie, zavolá sa UnhandledPromiseRejectionWarning.

Asynchrónne funkcie v spätných volaniach

Async funkcie sú pomerne často používané v .map alebo .filter ako spätné volanie. Príkladom je funkcia fetchPublicReposCount(username), ktorá vracia počet otvorených úložísk na GitHub. Povedzme, že existujú traja používatelia, ktorých metriky potrebujeme. Tu je kód pre túto úlohu:

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'];
}

Potrebujeme účty ArfatSalman, octocat, norvig. V tomto prípade robíme:

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

Stojí za to venovať pozornosť Await v spätnom volaní .map. Tu sa počíta množstvo sľubov a .map je anonymné spätné volanie pre každého konkrétneho používateľa.

Príliš dôsledné používanie čakania

Zoberme si tento kód ako príklad:

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;
}

Tu sa repo číslo umiestni do premennej count, potom sa toto číslo pridá do poľa counts. Problém s kódom je, že kým zo servera neprídu údaje prvého používateľa, všetci ďalší používatelia budú v pohotovostnom režime. Naraz sa teda spracúva iba jeden používateľ.

Ak napríklad spracovanie jedného používateľa trvá približne 300 ms, pre všetkých používateľov je to už sekunda, čas strávený lineárne závisí od počtu používateľov. Ale keďže získanie počtu repo nezávisí na sebe, procesy môžu byť paralelizované. Vyžaduje si to prácu s .map a Promise.all:

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

Promise.all dostane množstvo sľubov ako vstup a vráti sľub. Ten po dokončení všetkých prísľubov v poli alebo pri prvom odmietnutí je splnený. Môže sa stať, že sa nespustia všetky naraz - aby ste zabezpečili súčasné spustenie, môžete použiť p-mapu.

Záver

Asynchrónne funkcie sú pre vývoj čoraz dôležitejšie. Na adaptívne používanie asynchrónnych funkcií by ste mali použiť Asynchrónne iterátory. Vývojár JavaScriptu by sa v tom mal dobre vyznať.

Skillbox odporúča:

Zdroj: hab.com

Pridať komentár