Autor článku vysvětluje na příkladech Async/Await v JavaScriptu. Obecně řečeno, Async/Await je pohodlný způsob psaní asynchronního kódu. Před zavedením této funkce byl podobný kód psán pomocí zpětných volání a promissů. Autor původního článku odhaluje výhody Async/Await a rozebírá různé příklady.
Připomínáme: pro všechny čtenáře "Habra" – sleva 10 000 korun při registraci na jakýkoli kurz Skillbox s promokódem "Habr".
Skillbox doporučuje: Vzdělávací online kurz .
Zpětné volání
Zpětné volání představuje funkci, jejíž vyvolání je odloženo na neurčitou dobu. Dříve se zpětná volání používala v těch částech kódu, kde výsledek nemohl být získán okamžitě.
Zde je příklad asynchronního čtení souboru na Node.js:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
}); Problémy nastávají ve chvíli, kdy je třeba provést několik asynchronních operací najednou. Představme si následující scénář: provádí se dotaz do databáze uživatele Arfat, je třeba načíst jeho pole profile_img_url a stáhnout obrázek ze serveru someserver.com.
Po stažení konvertujeme obrázek do jiného formátu, například z PNG na JPEG. Pokud konverze proběhne úspěšně, uživateli se odešle e-mail. Poté se informace o události zapíše do souboru transformations.log s uvedením data.
Je třeba věnovat pozornost propletenosti zpětných volání a velkému množství }) na závěru kódu. Tomu se říká Callback Hell nebo Pyramid of Doom.
Nevýhody tohoto způsobu jsou zřejmé:
- Tento kód je obtížné číst.
- Také je obtížné zpracovávat chyby, což často vede k horší kvalitě kódu.
Aby se tento problém vyřešil, byly do JavaScriptu přidány promisy. Ty umožňují nahradit hluboké vnoření zpětných volání slovem .then.
Pozitivní stránkou promisů je, že s nimi se kód čte mnohem lépe, a to shora dolů, spíše než zleva doprava. Nicméně i promisy mají své problémy:
- Je nutné přidávat velké množství .then.
- Místo try/catch se používá .catch pro zpracování všech chyb.
- Práce s několika promisy v rámci jednoho cyklu není daleko vždy pohodlná, v některých případech ztěžují kód.
Toto je úkol, který ukáže význam posledního bodu.
Předpokládejme, že existuje cyklus for, který vypisuje sekvenci čísel od 0 do 10 s náhodným intervalem (0–n sekund). Pomocí promissů je třeba tento cyklus změnit tak, aby čísla byla vytištěna v sekvenci od 0 do 10. Takže, pokud vydání nuly trvá 6 sekund a jedničky 2 sekundy, nejprve musí být vytištěna nula a poté začne odpočet vydání jedničky.
A samozřejmě, k vyřešení tohoto úkolu nepoužíváme Async/Await ani .sort. Příklad řešení je na konci.
Async funkce
Přidání async funkcí v ES2017 (ES8) zjednodušilo práci s promisy. Všimněte si, že async funkce fungují „nad“ promisy. Tyto funkce nepředstavují kvalitativně jiné koncepty. Async funkce byly zamýšleny jako alternativa kódu, který používá promisy.
Async/Await umožňuje uspořádat práci s asynchronním kódem ve synchronním stylu.
Takže znalost promisů usnadňuje pochopení principů Async/Await.
Syntaxe
V běžné situaci se skládá ze dvou klíčových slov: async a await. První slovo převádí funkci na asynchronní. V takových funkcích je povoleno použití await. V jakémkoli jiném případě použití této funkce vyvolá 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 začátek deklarace funkce, a v případě použití šipkové funkce mezi znakem „=” a závorkami.
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 si pamatovat, že konstruktory tříd a gettery/settery nemohou být asynchronní.
Semantika a pravidla provádění
Async funkce jsou v zásadě podobné standardním JS funkcím, ale existují výjimky.
Takže async funkce vždy vracejí promisy:
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloKonkrétně fn vrací řetězec hello. A protože je to asynchronní funkce, je hodnota řetězce zabalena do promisu pomocí konstruktora.
Zde je alternativní konstrukce bez Async:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// helloV tomto případě je vracení promisu prováděno „ručně“. Asynchronní funkce je vždy zabalena do nového promisu.
Pokud je vrácená hodnota primitiv, async-funkce ji vrátí tak, že ji obalí do promise. Pokud je vrácená hodnota sama o sobě objektem promise, její vyřešení bude vráceno v novém promise.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Ale co se stane, pokud dojde k chybě uvnitř asynchronní funkce?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);Pokud nebude zpracována, foo() vrátí promise s rejeckem. V této situaci místo Promise.resolve bude vrácen Promise.reject, který obsahuje chybu.
Async-funkce vždy vrací promise, bez ohledu na to, co je vráceno.
Asynchronní funkce se pozastavují při každém await.
Await ovlivňuje výrazy. Pokud je výraz promise, async-funkce se pozastaví do vyřešení promise. Pokud výraz není promise, konvertuje se na promise přes Promise.resolve a poté skončí.
// 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 tady je popis toho, jak fn-funkce funguje.
- Po jejím zavolání se první řádek konvertuje z const a = await 9; na const a = await Promise.resolve(9);.
- Po použití Await se vykonání funkce pozastaví, dokud a nezíská svou hodnotu (v této situaci to je 9).
- delayAndGetRandom(1000) pozastavuje vykonání fn-funkce, dokud se sama nedokončí (po 1 sekundě). To je vlastně pozastavení fn-funkce na 1 sekundu.
- delayAndGetRandom(1000) vrací náhodnou hodnotu přes resolve, která se poté přiřadí proměnné b.
- A případ s proměnnou c je analogický případu s proměnnou a. Poté se vše pozastaví na sekundu, ale nyní delayAndGetRandom(1000) nic nevrací, protože to není potřeba.
- Výsledky se počítají podle vzorce a + b * c. Výsledek je obalen do promise pomocí Promise.resolve a vrácen funkcí.
Tyto pauzy mohou připomínat generátory v ES6, ale důvod je .
Řešíme úlohu
Nyní se podívejme na řešení úlohy, která byla uvedena výše.
Ve funkci finishMyTask se používá Await k čekání na výsledky operací jako queryDatabase, sendEmail, logTaskInFile a dalších. Pokud toto řešení porovnáme s použitím promissů, je jasné, že jsou si podobné. Nicméně verze s Async/Await značně zjednodušuje syntaktické složitosti. V tomto případě není potřeba množství callbacků a řetězců jako .then/.catch.
Zde je řešení s výstupem čísel, existují zde dvě varianty.
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 nyní je zde řešení s použitím asynchronních funkcí.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}Zpracování chyb
Nezpracované chyby jsou zabaleny do zamítnutého promisu. Nicméně v asynchronních funkcích lze použít konstrukci try/catch pro synchronní zpracování chyb.
async function canRejectOrReturn() {
// čekat jednu sekundu
await new Promise(res => setTimeout(res, 1000));
// Zamítnout s ~50% pravděpodobností
if (Math.random() > 0.5) {
throw new Error('Omlouváme se, číslo je příliš velké.')
}
return 'dokonalé číslo';
}canRejectOrReturn() je asynchronní funkce, která buď úspěšně vrací („dokonalé číslo”), nebo končí s chybou („Omlouváme se, číslo je příliš velké”).
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'chyba zachycena';
}
}Jelikož v předchozím příkladu očekáváme provedení canRejectOrReturn, neúspěšné dokončení vyústí v provedení bloku catch. Výsledkem bude, že funkce foo skončí buď s undefined (když se v bloku try nic nevrací), nebo s chybou 'chyba zachycena'. Funkce tedy nebude mít neúspěšné dokončení, protože try/catch se postará o zpracování samotné funkce foo.
Zde je další příklad:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'chyba zachycena';
}
}Je důležité si všimnout, že v příkladu z foo se vrací canRejectOrReturn. Foo pak skončí buď s dokonalým číslem, nebo vrátí chybu 'Error' („Omlouváme se, číslo je příliš velké”). Blok catch nikdy nebude vykonán.
Problém je v tom, že foo vrací promiss předaný z canRejectOrReturn. Řešení funkce foo se tak stává řešením pro canRejectOrReturn. V tomto případě bude kód složen pouze ze dvou řádků:
try {
const promise = canRejectOrReturn();
return promise;
}A co se stane, pokud použijeme await a return spolu:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'chyba zachycena';
}
}V výše uvedeném kódu foo skončí buď s dokonalým číslem, nebo s 'chybou zachycena'. Zde k žádnému selhání nedojde. Funkce foo skončí s canRejectOrReturn, a ne s undefined. Ujistěme se o tom tím, že odstraníme řádek return await canRejectOrReturn():
try {
const value = await canRejectOrReturn();
return value;
}
// …Běžné chyby a úskalí
V některých případech může použití Async/Await vést k chybám.
Zapomenuté await
K tomu dochází poměrně často — před promisem se zapomene klíčové slovo await:
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'chyceno';
}
}V kódu, jak vidno, není ani await, ani return. Proto foo vždy končí s undefined bez zpoždění 1 sekundy. Ale promisem se bude vykonávat. Pokud však dojde k chybě nebo odmítnutí, v tomto případě dojde k vyvolání UnhandledPromiseRejectionWarning.
Async funkce v callbacky
Async funkce se poměrně často používají v .map nebo .filter jako callbacky. Jako příklad můžeme vzít funkci fetchPublicReposCount(username), která vrací počet veřejných repozitářů na GitHubu. Předpokládejme, že máme tři uživatele, jejichž čísla potřebujeme. Tady 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ě vykonáme:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});Je dobré si všimnout await v callbacku .map. Zde counts je pole promisu, a .map je anonymní callback pro každého uvedeného uživatele.
Nadměrné sekvenční používání await
Jako příklad vezmeme takový kód:
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 do proměnné count umístěno číslo repozitářů, pak je toto číslo přidáno do pole counts. Problém kódu je ten, že dokud nepřijdou data prvního uživatele ze serveru, všichni následující uživatelé budou čekat. Tím pádem je v jednom okamžiku zpracován pouze jeden uživatel.
Pokud například zpracování jednoho uživatele trvá přibližně 300 ms, pak pro všechny uživatele by to již byla sekunda, čas strávený je lineárně závislý na počtu uživatelů. Ale protože získání počtu repozitářů není na sobě závislé, procesy lze paralelizovat. K tomu je třeba pracovat s .map a Promise.all:
asynchronní funkce fetchAllCounts(users) {
const promises = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
return Promise.all(promises);
}Promise.all na vstupu přijímá pole promissů, které vrací promiss. Ten se uzavře až po dokončení všech promissů v poli nebo při prvním odmítnutí. Může se stát, že se všechny nespustí současně; pro zajištění současného spuštění lze použít p-map.
Závěr
Asynchronní funkce se stávají čím dál důležitějšími pro vývoj. Pro adaptivní využití asynchronních funkcí je dobré se ujistit, že . JavaScriptový vývojář by se v tom měl dobře orientovat.
Skillbox doporučuje:
- Praktický kurz .
- Aplikovaný online kurz .
- Dvouroční praktický kurz .
Zdroj: habr.com
