Podívejme se na Async/Await v JavaScriptu na příkladech

Autor článku zkoumá příklady Async/Await v JavaScriptu. Celkově je Async/Await pohodlný způsob, jak psát asynchronní kód. Než se tato funkce objevila, byl takový kód napsán pomocí zpětných volání a slibů. Autor původního článku odhaluje výhody Async/Await analýzou různých příkladů.

Připomínáme: pro všechny čtenáře "Habr" - sleva 10 000 rublů při zápisu do jakéhokoli kurzu Skillbox pomocí propagačního kódu "Habr".

Skillbox doporučuje: Vzdělávací online kurz "Java developer".

Zpětné volání

Callback je funkce, jejíž volání je odloženo na neurčito. Dříve se zpětná volání používala v těch oblastech kódu, kde nebylo možné okamžitě získat výsledek.

Zde je příklad asynchronního čtení souboru v Node.js:

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

Problémy nastávají, když potřebujete provést několik asynchronních operací najednou. Představme si tento scénář: je ​​zadán požadavek na databázi uživatelů Arfat, musíte si přečíst jeho pole profile_img_url a stáhnout obrázek ze serveru someserver.com.
Po stažení obrázek převedeme do jiného formátu, například z PNG do JPEG. Pokud byla konverze úspěšná, je uživateli zaslán dopis na e-mail. Dále se do souboru transforms.log zadají informace o události s uvedením data.

Za pozornost stojí překrývání zpětných volání a velký počet }) v závěrečné části kódu. Jmenuje se Callback Hell nebo Pyramid of Doom.

Nevýhody této metody jsou zřejmé:

  • Tento kód je obtížně čitelný.
  • Je také obtížné zvládnout chyby, což často vede ke špatné kvalitě kódu.

K vyřešení tohoto problému byly do JavaScriptu přidány sliby. Umožňují vám nahradit hluboké vnoření zpětných volání slovem .then.

Pozitivním aspektem slibů je, že kód činí mnohem lépe čitelným, spíše shora dolů než zleva doprava. Sliby však mají také své problémy:

  • Musíte přidat hodně .pak.
  • Místo try/catch se ke zpracování všech chyb používá .catch.
  • Práce s více přísliby v rámci jedné smyčky není vždy pohodlná, v některých případech komplikují kód.

Zde je problém, který ukáže význam posledního bodu.

Předpokládejme, že máme cyklus for, který tiskne posloupnost čísel od 0 do 10 v náhodných intervalech (0–n sekund). Pomocí slibů musíte změnit tuto smyčku tak, aby se čísla tiskla v pořadí od 0 do 10. Pokud tedy tisk nuly trvá 6 sekund a tisk jedničky 2 sekundy, měla by se nejprve vytisknout nula a poté začne odpočítávání pro tisk jedničky.

A k vyřešení tohoto problému samozřejmě nepoužíváme Async/Await nebo .sort. Příklad řešení je na konci.

Asynchronní funkce

Přidání asynchronních funkcí v ES2017 (ES8) zjednodušilo práci se sliby. Podotýkám, že asynchronní funkce fungují „nad“ sliby. Tyto funkce nepředstavují kvalitativně odlišné pojmy. Asynchronní funkce jsou určeny jako alternativa ke kódu, který používá sliby.

Async/Await umožňuje organizovat práci s asynchronním kódem v synchronním stylu.

Znalost slibů tedy usnadňuje pochopení principů Async/Await.

syntax

Obvykle se skládá ze dvou klíčových slov: async a wait. První slovo změní funkci na asynchronní. Takové funkce umožňují použití čekání. V každém jiném případě použití této funkce 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 se vkládá na úplný začátek deklarace funkce a v případě funkce šipky mezi znaménko „=“ a závorky.

Tyto funkce lze umístit do objektu jako metody nebo použít v deklaraci třídy.

// 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 připomenout, že konstruktory tříd a gettery/settery nemohou být asynchronní.

Sémantika a pravidla provádění

Asynchronní funkce jsou v zásadě podobné standardním funkcím JS, ale existují výjimky.

Asynchronní funkce tedy vždy vrátí sliby:

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

Konkrétně fn vrací řetězec hello. Protože se jedná o asynchronní funkci, je hodnota řetězce zabalena do příslibu pomocí konstruktoru.

Zde je alternativní design bez Async:

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

V tomto případě je příslib vrácen „ručně“. Asynchronní funkce je vždy zabalena do nového příslibu.

Pokud je návratová hodnota primitivní, asynchronní funkce vrátí hodnotu tak, že ji zabalí do příslibu. Pokud je návratová hodnota objektem slibu, je jeho rozlišení vráceno v novém slibu.

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

Co se ale stane, když dojde k chybě uvnitř asynchronní funkce?

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

Pokud není zpracován, foo() vrátí slib s odmítnutím. V této situaci bude místo Promise.resolve vrácen Promise.reject obsahující chybu.

Asynchronní funkce vždy vydávají příslib, bez ohledu na to, co je vráceno.

Asynchronní funkce se pozastaví při každém čekání.

Čekání ovlivňuje výrazy. Pokud je tedy výraz příslib, asynchronní funkce je pozastavena, dokud není příslib splněn. Pokud výraz není slib, je převeden na slib přes Promise.resolve a poté dokončen.

// 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 zde je popis, jak funguje funkce fn.

  • Po jeho zavolání je první řádek převeden z const a = wait 9; in const a = wait Promise.resolve(9);.
  • Po použití Await je provádění funkce pozastaveno, dokud a nezíská svou hodnotu (v aktuální situaci je to 9).
  • delayAndGetRandom(1000) pozastaví provádění funkce fn, dokud se nedokončí samo (po 1 sekundě). Tím fakticky zastavíte funkci fn na 1 sekundu.
  • delayAndGetRandom(1000) prostřednictvím resolve vrací náhodnou hodnotu, která je pak přiřazena proměnné b.
  • No, případ s proměnnou c je podobný jako s proměnnou a. Poté se vše na sekundu zastaví, ale nyní delayAndGetRandom(1000) nevrací nic, protože to není vyžadováno.
  • V důsledku toho se hodnoty vypočítají pomocí vzorce a + b * c. Výsledek je zabalen do příslibu pomocí Promise.resolve a vrácen funkcí.

Tyto pauzy mohou připomínat generátory v ES6, ale něco na tom je vaše důvody.

Řešení problému

No a nyní se podíváme na řešení výše zmíněného problému.

Funkce finishMyTask používá Await k čekání na výsledky operací, jako je queryDatabase, sendEmail, logTaskInFile a další. Pokud porovnáte toto řešení s tím, kde byly použity sliby, podobnosti budou zřejmé. Verze Async/Await však značně zjednodušuje všechny syntaktické složitosti. V tomto případě nedochází k velkému počtu zpětných volání a řetězců jako .then/.catch.

Zde je řešení s výstupem čísel, jsou dvě 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 zde je řešení pomocí asynchronních funkcí.

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

Chyba při zpracování

Neošetřené chyby jsou zabaleny do odmítnutého slibu. Asynchronní funkce však mohou používat try/catch k synchronnímu zpracování chyb.

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 asynchronní funkce, která buď uspěje („dokonalé číslo“), nebo selže s chybou („Omlouváme se, příliš velké číslo“).

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

Protože výše uvedený příklad očekává provedení canRejectOrReturn, jeho vlastní selhání bude mít za následek provedení bloku catch. Výsledkem je, že funkce foo skončí buď jako nedefinovaná (když se v bloku try nic nevrátí), nebo se zachycenou chybou. V důsledku toho tato funkce neselže, protože try/catch zvládne funkci foo sama.

Zde je další příklad:

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

Stojí za to věnovat pozornost skutečnosti, že v příkladu je canRejectOrReturn vrácen z foo. Foo v tomto případě buď skončí s dokonalým číslem, nebo vrátí chybu („Promiň, číslo je příliš velké“). Blok catch nebude nikdy proveden.

Problém je v tom, že foo vrací slib předaný z canRejectOrReturn. Takže řešení pro foo se stává řešením pro canRejectOrReturn. V tomto případě bude kód obsahovat pouze dva řádky:

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

Co se stane, když použijete wait a return společně:

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

Ve výše uvedeném kódu bude foo úspěšně ukončen s dokonalým číslem a zachycenou chybou. Tady nebudou žádná odmítnutí. Ale foo se vrátí s canRejectOrReturn, ne s undefined. Přesvědčte se o tom odstraněním řádku return wait canRejectOrReturn():

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

Časté chyby a úskalí

V některých případech může použití Async/Await vést k chybám.

Zapomenuté čekání

To se stává poměrně často – klíčové slovo wait je zapomenuto před slibem:

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

Jak vidíte, v kódu není žádné čekání ani návrat. Proto foo vždy odejde s nedefinovaným bez 1 sekundového zpoždění. Ale slib bude splněn. Pokud vyvolá chybu nebo odmítnutí, zavolá se UnhandledPromiseRejectionWarning.

Asynchronní funkce ve zpětných voláních

Async funkce jsou poměrně často používány v .map nebo .filter jako zpětná volání. Příkladem je funkce fetchPublicReposCount(username), která vrací počet otevřených úložišť na GitHubu. Řekněme, že existují tři uživatelé, jejichž metriky potřebujeme. Zde je kód pro tento úkol:

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

Potřebujeme účty ArfatSalman, Octocat, Norvig. V tomto případě děláme:

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

Stojí za to věnovat pozornost Await ve zpětném volání .map. Zde se počítá řada slibů a .map je anonymní zpětné volání pro každého konkrétního uživatele.

Příliš důsledné používání wait

Vezměme si tento kód jako pří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;
}

Zde je číslo repo umístěno do proměnné count, poté je toto číslo přidáno do pole counts. Problém s kódem je, že dokud data prvního uživatele nedorazí ze serveru, budou všichni další uživatelé v pohotovostním režimu. Zpracovává se tedy vždy pouze jeden uživatel.

Pokud například zpracování jednoho uživatele trvá cca 300 ms, pak u všech uživatelů je to již sekunda, čas strávený lineárně závisí na počtu uživatelů. Ale protože získání počtu repo na sobě nezávisí, lze procesy paralelizovat. To vyžaduje práci 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 obdrží řadu slibů jako vstup a vrátí slib. To druhé, po dokončení všech příslibů v poli nebo při prvním odmítnutí, je dokončeno. Může se stát, že se nespustí všechny najednou - pro zajištění současného spuštění můžete použít p-mapu.

Závěr

Asynchronní funkce jsou pro vývoj stále důležitější. Pro adaptivní použití asynchronních funkcí byste měli použít Asynchronní iterátory. Vývojář JavaScriptu by se v tom měl dobře orientovat.

Skillbox doporučuje:

Zdroj: www.habr.com

Přidat komentář