Autor článku analyzuje na príkladoch Async/Await v JavaScripte. Celkovo, Async/Await je pohodlný spôsob, ako písať asynchrónny kód. Predtým sa takýto kód písal s použitím spätných volaní a promísov. Autor originálneho článku odhaľuje výhody Async/Await prácou na rôznych príkladoch.
Pripomíname: pre všetkých čitateľov „Habr“ — zľava 10 000 rubľov pri zapísaní na akýkoľvek kurz Skillbox s promo kódom „Habr“.
Skillbox odporúča: Vzdelávací online kurz .
Spätné volanie
Spätné volanie predstavuje funkciu, ktorej zavolanie je odložené na neurčitý čas. Predtým sa spätné volania používali na miestach v kóde, kde výsledok nemohol byť získaný okamžite.
Tu je príklad asynchrónneho čítania súboru na Node.js:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
}); Problémy nastávajú v momente, keď je potrebné vykonať viacero asynchrónnych operácií. Predstavme si takýto scenár: vykonáva sa dopyt v databáze používateľa Arfat, potrebujeme prečítať jeho pole profile_img_url a načítať obrázok zo servera someserver.com.
Po načítaní konvertujeme obrázok do iného formátu, napríklad z PNG do JPEG. Ak konverzia prebehla úspešne, na e-mail používateľa sa posiela správa. Potom sa informácie o udalosti zaznamenajú do súboru transformations.log s uvedením dátumu.
Je potrebné venovať pozornosť prekrývaniu spätných volaní a veľkému množstvu }) na konci kódu. To sa nazýva Callback Hell alebo Pyramid of Doom.
Nevýhody takéhoto spôsobu sú zjavné:
- Tento kód je ťažko čitateľný.
- Je v ňom tiež ťažké spracovávať chyby, čo často vedie k zhoršeniu kvality kódu.
Aby sme vyriešili tento problém, promisy boli pridané do JavaScriptu. Umožňujú nahradiť hlboké vnorovanie spätných volaní slovom .then.
Pozitívny aspekt promísov je, že s nimi je kód oveľa čitateľnejší, pritom zhora nadol, a nie zľava doprava. Napriek tomu aj promisy majú svoje problémy:
- Je potrebné pridávať veľké množstvo .then.
- Namiesto try/catch sa používa .catch na spracovanie všetkých chýb.
- Práca s niekoľkými promismi v rámci jedného cyklu nie je vždy pohodlná, v niektorých prípadoch komplikuje kód.
Tu je úloha, ktorá ukáže význam posledného bodu.
Predpokladajme, že existuje cyklus for, ktorý vypisuje sekvenciu čísel od 0 do 10 s náhodným intervalom (0–n sekúnd). Pomocou prísľubov je potrebné upraviť tento cyklus tak, aby čísla boli vypisované v sekvencii od 0 do 10. Takže, ak vypísanie nuly trvá 6 sekúnd a jednotky 2 sekundy, najprv sa vypíše nula a potom začne odpočítavanie pre jednotku.
A samozrejme, na vyriešenie tejto úlohy nepoužívame Async/Await ani .sort. Príklad riešenia bude na konci.
Async funkcie
Pridanie async funkcií do ES2017 (ES8) zjednodušilo prácu s prísľubmi. Zdôrazňujem, že async funkcie fungujú „nad“ prísľubmi. Tieto funkcie nie sú kvalitne iné koncepty. Async funkcie boli navrhnuté ako alternatíva kódu, ktorý používa prísľuby.
Async/Await umožňuje organizovať prácu s asynchrónnym kódom v synchronnom štýle.
Tým pádom, znalosť prísľubov uľahčuje pochopenie princípov Async/Await.
Syntax
V bežnej situácii pozostáva z dvoch kľúčových slov: async a await. Prvé slovo premieňa funkciu na asynchrónnu. V takýchto funkciách je povolené používanie await. V akomkoľvek inom prípade použitie tejto funkcie spôsobí 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 vkladajú na samotnom začiatku vyhlásenia funkcie, a v prípade použití šípovej funkcie medzi znakom „=” a zátvorkami.
Tieto funkcie je možné umiestniť do objektu ako metódy alebo ich využiť pri vyhlásení 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! Treba mať na pamäti, že konštruktory tried a gettery/settery nemôžu byť asynchrónne.
Sémantika a pravidlá vykonávania
Async funkcie sú v podstate podobné štandardným JS funkciám, ale sú aj výnimky.
Tak, async funkcie vždy vracajú prísľuby:
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloKonkrétne, fn vracia reťazec hello. A pretože je to asynchrónna funkcia, hodnota reťazca je zabalená do prísľubu pomocou konštruktora.
Tu je alternatívna konštrukcia bez Async:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// helloV tomto prípade je vrátenie prísľubu vykonávané „ručne“. Asynchrónna funkcia je vždy zabalená do nového prísľubu.
V prípade, že sa vrátna hodnota — primitív, async-funkcia vracia hodnotu tým, že ju zabalí do promisu. Ak je vrátna hodnota objektom promisu, jeho riešenie sa vráti v novom promise.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Ale čo sa stane, ak sa vnútri asynchrónnej funkcie objaví chyba?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);Ak nebude spracovaná, foo() vráti promise s rejekciou. V tejto situácii sa namiesto Promise.resolve vráti Promise.reject, ktorý obsahuje chybu.
Async-funkcie vždy produkujú promise, bez ohľadu na to, čo sa vracia.
Asynchrónne funkcie sa pozastavujú pri každom await.
Await ovplyvňuje výrazy. Ak je teda výraz promise, async-funkcia sa pozastaví, kým sa promise vykoná. Ak výraz nie je promise, konvertuje sa na promise 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 toho, ako funguje fn-funkcia.
- Po jej zavolaní sa prvý riadok konvertuje z const a = await 9; na const a = await Promise.resolve(9);.
- Po použití Await sa vykonanie funkcie pozastaví, kým a nezíska svoju hodnotu (v tomto prípade je to 9).
- delayAndGetRandom(1000) pozastavuje vykonanie fn-funkcie, kým sa táto funkcia nedokončí (po 1 sekunde). To je v podstate zastavenie fn-funkcie na 1 sekundu.
- delayAndGetRandom(1000) vracia cez resolve náhodnú hodnotu, ktorá sa potom priradí premennej b.
- Pokiaľ ide o variabilitu c, je to analogické s prípadem premennej a. Potom sa všetko pozastaví na sekundu, ale teraz delayAndGetRandom(1000) nič nevracia, pretože to nie je potrebné.
- Celkovo sa hodnoty vyhodnocujú podľa vzorca a + b * c. Výsledok sa potom zabalí do promisu pomocou Promise.resolve a vráti sa funkciou.
Tieto prestávky môžu pripomínať generátory v ES6, ale má to .
Riešime úlohu
A teraz sa pozrime na riešenie úlohy, ktorá bola uvedená vyššie.
Vo funkcii finishMyTask sa používa Await na čakanie na výsledky takých operácií ako queryDatabase, sendEmail, logTaskInFile a iných. Ak porovnáme toto riešenie s tým, kde sa používali promisy, je zjavná podobnosť. Napriek tomu verzia s Async/Await značne zjednodušuje všetky syntaktické zložitosti. V tomto prípade nie je veľké množstvo callbackov a reťazcov ako .then/.catch.
Tu je riešenie s výstupom čísel, existujú dva 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 tu je riešenie s použitím async-funkcií.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}Správa chýb
Nezpracované chyby sú zabalené do rejected promis. Napriek tomu v async-funkciách je možné použiť konštrukciu try/catch na vykonanie synchronizovanej správy chýb.
async function canRejectOrReturn() {
// čakanie jednu sekundu
await new Promise(res => setTimeout(res, 1000));
// Odmietnuť s ~50% pravdepodobnosťou
if (Math.random() > 0.5) {
throw new Error('Je mi ľúto, číslo je príliš veľké.')
}
return 'dokonalé číslo';
}canRejectOrReturn() je asynchrónna funkcia, ktorá buď úspešne vykoná („dokonalé číslo“), alebo neúspešne skončí s chybou („Je mi ľúto, číslo je príliš veľké“).
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'chyba zachytená';
}
}Keďže v príklade vyššie sa očakáva vykonanie canRejectOrReturn, jeho vlastné neúspešné skončenie vyvolá vykonanie bloku catch. Ako výsledok funkcia foo skončí buď s undefined (keď v bloku try sa nič nevracia), alebo s chybou chytenou. V dôsledku toho táto funkcia nebude mať neúspešné skončenie, pretože try/catch sa postará o spracovanie samotnej funkcie foo.
Tu je ďalší príklad:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'chyba zachytená';
}
}Stojí za to venovať pozornosť tomu, že v príklade z foo sa vracia canRejectOrReturn. Foo v tomto prípade končí buď dokonalým číslom, alebo sa vráti chyba Error („Je mi ľúto, číslo je príliš veľké“). Blok catch sa nikdy nevykoná.
Problém je v tom, že foo vracia promis, ktorý bol odovzdaný z canRejectOrReturn. Preto sa riešenie funkcie foo stáva riešením pre canRejectOrReturn. V takomto prípade bude kód pozostávať iba zo dvoch riadkov:
try {
const promise = canRejectOrReturn();
return promise;
}A tu je, čo sa stane, ak sa použije spolu await a return:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'chyba zachytená';
}
}V uvedenom kóde bude foo úspešne dokončené ako s perfect number, tak aj s error caught. Tu nedôjde k zlyhaniam. Ale foo skončí s canRejectOrReturn, a nie s undefined. Poďme sa o tom presvedčiť tým, že odstránime riadok return await canRejectOrReturn():
try {
const value = await canRejectOrReturn();
return value;
}
// …Bežné chyby a nástrahy
V niektorých prípadoch môže použitie Async/Await viesť k chybám.
Zabudnuté await
To sa stáva dosť často — pred promisiou sa zabudne kľúčové slovo await:
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}V kóde, ako vidno, nie je ani await, ani return. Preto foo vždy končí s undefined bez 1 sekundy oneskorenia. Ale promise bude vykonávaný. Ak však vyhodí chybu alebo bude rejected, v tomto prípade sa vyvolá UnhandledPromiseRejectionWarning.
Async funkcie v spätnej väzbe
Async funkcie sa často používajú v .map alebo .filter ako callbacky. Ako príklad si môžeme uviesť funkciu fetchPublicReposCount(username), ktorá vracia počet verejných repozitárov na GitHube. Predpokladajme, že máme troch používateľov, ktorých výsledky sú pre nás dôležité. 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 vykonáme:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});Stojí za zmienku Await v spätnom volaní .map. Tu counts je pole promise, a .map je anonymné spätné volanie pre každého uvedeného používateľa.
Nadmerné sekvenčné používanie await
Ako príklad vezmime takýto 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;
}Tu sa do premennej count ukladá počet repozitárov, následujúce toto číslo sa pridáva do poľa counts. Problém s kódom je v tom, že pokiaľ neprídu dáta prvého používateľa zo servera, všetci nasledujúci používatelia budú v režime čakania. Takto sa v jednom okamihu spracováva len jeden používateľ.
Ak je napríklad na spracovanie jedného používateľa potrebných približne 300 ms, potom pre všetkých používateľov je to už sekunda, pričom čas potrebný na spracovanie závisí od počtu používateľov. Avšak, keďže získavanie počtu repozitárov nezávisí jeden od druhého, procesy je možné paralelizovať. Na to je potrebné pracovať 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 na vstupe prijíma pole s prísľubmi, pričom vráti prísľub. Posledný sa ukončí po dokončení všetkých prísľubov v poli alebo pri prvom odmietnutí. Môže sa stať, že všetky sa nespustia súčasne — na zaistenie súčasného spustenia je možné použiť p-map.
Záver
Async funkcie sa stávajú čoraz dôležitejšími pre vývoj. A na adaptívne využitie async funkcií by stálo za to využiť . JavaScript vývojár by mal mať dobré znalosti o tom.
Skillbox odporúča:
- Praktický kurz .
- Aplikovaný online kurz .
- Dvojročný praktický kurz .
Zdroj: habr.com
