Publikoval Michal Kočí dňa 24.9.2018 o 09:00 v kategóriach Node.js a JavaScript
V predchádzajúcom príspevku tejto mini série sme sa naučili pracovať s promismi. Dnes sa naučíme používať tzv. async funkcie použitím nových klúčových slov async a await. Pre správne pochopenie async funkcií potrebujete dobrú znalosť promisov, nakoľko sú na nich postavené, preto sme sa im venovali minule a dnes na to nadviažeme.
Vráťme sa na chvíľu k prvému dielu, kde sme si povedali nepríjemný dôsledok asynchronnosti: nemožnosť napísania funkcie, ktorá spúšťa asynchrónny kód a zároveň vracia výsledok tejto asychnrónnej činnosti.
Prečo je to vlastne problém? Veď v zásade je nám jedno, či nám funkcia výsledok asynchrónnej činnosti vráti ako svoju návratovú hodnotu, príjme callback, ktorý zavolá keď bude hotovo alebo vráti promise, ku ktorému si callback pridáme my. Či nie je?
Teoreticky nám to jedno je. Alebo môže byť. Veď v každej z týchto troch možností musíme aj tak počkať na výsledok asynchrónnej činnosti a až potom môžeme pokračovať...
Ale zamyslime sa nad týmito tromi možnosťami. Ktorá je nám mentálne najpríjemnejšia, najmilšia, najintuitívnejšia a radi by sme ju preferovali?
Tak ktorá je preferovaná?
Samozrejme prvá, t.j. návratová hodnota. A dôvod? Náš mentálny model. To ako sme zvyknutý mentálne s kódom pracovať. To ako ho čítame. Odhora nadol. Riadok po riadku. Príkaz za príkazom.
Pozrime sa na tieto tri príklady, aby sme sa v tomto utvrdili. Pozrieme sa nie na funkciu loadData
, ale na jej použitie.
Začnime callbackmi:
console.log('start');
let url = 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH&tsyms=EUR';
loadData(url, function (err, data) {
if(err) {
console.log('async failed');
console.error(err);
} else {
console.log('async done');
console.log(data);
}
})
console.log('end');
Ak ste na tom podobne ako väčšina z nás, tak budete čítať kód odhora dole a mentálne vám nebude robiť dobre, že do konzoly sa najskôr vypíše start
, potom end
a až nakoniec async done
. Tým, ako čítame kód, stále nás to vedie k očakávaniu, že end sa vypíše až na konci programu.
U promisov to nie je o moc iné:
console.log('start');
let url = 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH&tsyms=EUR';
loadData(url)
.then(function (data) {
console.log('async done');
console.log(data);
}, function (reason) {
console.log('async failed');
console.error(reason);
});
console.log('end');
Situácia je v zásade veľmi podobná, i keď použitie funkcie then
nám už napovedá, že kód bude zrejme zavolaný neskôr (then = potom). Ale stále, čitateľnosť je zhoršená, náš mozog proste očakáva sekvenčné poradie vykonávania kódu.
Čo ak by ale funkcia loadData
priamo dáta vracala? Ako návratovú hodnotu. Zatiaľ nám toto nebude fungovať, ale neskôr s menšími úpravami áno:
console.log('start');
let url = 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH&tsyms=EUR';
try {
let data = loadData(url)
console.log('async done');
console.log(data);
} catch(err) {
console.log('async failed');
console.error(err);
}
console.log('end');
Nie je to hneď lepšie? Sekvenčne nám to sedí a aj v konzole by išli informácie v poradí, v akom by sme ich chceli a očakávali - start
, async done
a end
.
Async alebo asychrónna funkcia je novinka v ES2017 (ES8) a jedná sa o funkciu pred ktorú pridáte klúčové slovo async
. Môže ísť o funkciu ale aj o arrow funkciu, ako vidno na týchto dvoch príkladoch:
async function loadData(url) {
/* ... */
}
const loadData = url => { /* ... */ }
Čím sa teda asynchrónna funkcia líši od synchrónnej? Čo vlastne kľúčové slovo async
znamená, čo spôsobuje?
async
označuje asynchrónnu funkciu a označuje, že táto funkcia vracia Promise
. Vždy vracia promise. Na to je hrozne dôležité pamätať.
Podľa toho teda k jej výsledku musíte pristupovať - bude to promise a tak bez akýchkoľvek dodatočných znalostí viete, že k jej výsledku sa dostanete spôsobom známym z minulého dielu - pripojením sa cez then
prípadne catch
:
async function loadData(url) {
/* ... */
}
let url = 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH&tsyms=EUR';
loadData(url)
.then(function (data) {
console.log(data);
}, function (reason) {
console.error(reason);
});
console.log('end');
Fajn, poviete si, tak aký to má pre mňa prínos? Na čo mi je to dobré, keď to stále vracia len promise? Keď sa na spôsobe použitia nič nezmenilo? Nuž, vydržte, sľubujem, že bude lepšie.
V rámci async funkcie môžete cez return
vrátiť ľubovoľnú hodnotu - objekt, číslo, reťazec, čokoľvek. Áno, tvrdil som, že async
funkcia vracia Promise
. A neklamal som. Vy cez return
vrátite čo chcete, návratová hodnota je však vždy Promise
.
V tomto momente už zrejme tušíte a niečo vám to pripomína. Možno vám to pripomína callback, ktorý zapisujete do then
. Aj z neho môžete vrátiť čokoľvek. A aj then
vždy vracia Promise
. A u async
funkcií to funguje podobne.
Takže, bez ďalšieho napínania, poďme na pravidlá, ktoré pre async funkciu platia.
Promise, ktorý vracia async funkcia bude naplnený (fulfilled
), ak v nej použijete return
a hodnota naplnenia bude to, čo cez return
budete vracať
async function returnObject() {
return { name: 'Michal' };
}
returnObject()
.then(function (data) { console.log(data.name); })
.catch(function (err) { console.error(err); });
V konzole sa nám objaví Michal, čo je pre nás dôkaz jednak toho, že async funkcia ozaj vracia Promise
(inak by nám nefungovali then
a catch
) a tiež aj toho, že promise bude naplnený s objektom, ktorý vraciame.
Promise, ktorý vracia async funkcia bude odmietnutý (rejected
), ak v nej vyvoláta vynímku štandardne cez throw
a hodnota odmietnutia bude práve to, čo vyvoláte cez throw
async function throwError() {
throw new Error('I always throw Error');
}
throwError()
.then(function (data) { console.log(data.name); })
.catch(function (err) { console.error(err); });
Opäť dôkaz namiesto sľubov (pekný dvojzmysel, pravda?) - V async
funkcii throw
-ujeme, takže promise, ktorý je z nej vrátený to prekonvertuje na rejectnutie s hodnotou toho, čo sme vyvolali. V našom prípade v konzole uvidíme I always throw Error
.
Promise, ktorý vracia async funkcia bude zrkadliť (mirrorovať
) hodnotu vráteného promisu, pokiaľ v nej použijete return
a vracať budete Promise
.
Vyššie uvedené príklady by sme podľa tohto pravidla mohli prepísať nasledovne a začneme príkladom, kedy vraciame cez return
Promise
ktorý bude (resp. je) naplnený:
async function returnObject() {
return Promise.resolve({ name: 'Michal' });
}
returnObject()
.then(function (data) { console.log(data.name); })
.catch(function (err) { console.error(err); });
A tu je prípad, kedy bude nami vracaný Promise
a aj Promise
vrátený z async
funkcie odmietnutý:
async function throwError() {
return Promise.reject(new Error('I always throw Error'));
}
throwError()
.then(function (data) { console.log(data.name); })
.catch(function (err) { console.error(err); });
Zatiaľ sme sa venovali iba jednej polovici z dvojice async
/await
. Poďme sa teraz pobaviť o druhej - o await
. Čo spôsobuje await
? Na čo nám slúži?
await
je klúčové slovo, ktoré slúži na pozastavenie vykonávania async funkcie, počkanie na settlement promisu a rozbalenie výsledku či vyvolanie vynímky.
Fajn, a teraz si túto definíciu rozoberme na drobné. Vychádzať budeme z toho, že máme funkciu loadData
, ktorá nám vracia Promise
. Áno, tú z minulého dielu:
function loadData(url) {
return new Promise(function (resolve, reject) {
let request = new XMLHttpRequest();
request.addEventListener('load', function() {
resolve(this.responseText);
});
request.addEventListener('error', function() {
reject(new Error('Failed to load data'));
});
request.open('GET', url);
request.send();
});
}
Začnime druhou časťou - slúži na počkanie na settlement promisu. Z tohto by nám mala vyplnynúť prvá zaujímavá vlastnosť. Klúčové slovo await
sa píše vľavo od výrazu, ktorý vracia Promise
.
Či tým výrazom je premenná alebo napríklad volanie funkcie, to je jedno. Musí to byť výraz, ktorý vracia Promise
.
Ak teda vieme, že máme funckiu loadData
, ktorá nám vracia Promise
, vieme, že môžeme použiť await
a malo by nám to počkať na settlement (t.j. fulfillment alebo rejection):
let url = 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH&tsyms=EUR';
console.log('start');
await loadData(url);
console.log('end');
Pokiaľ si tento príklad spustíme, uvidíme, že exekúcia počká na výsledok - že v konzole uvidíme hneď start
, ale end
uvidíme, až keď volanie endpointu dobehne volanie. Dobré, nie?
Počkať na výsledok je jedna vec, ale dostať sa k nemu je samozrejme druhá. A aj s týmto vám await
pomôže.
Nielenže vám teda počká na settlement, ale ak je settlement úspešný a jedná sa tak o fulfillment (naplnenie), hodnota naplneného promisu je rozbalená a vrátená vám ako hodnota volania funkcie.
Ak sa teda chceme dostať k dátam z loadData
, jednoducho si ich priradíme do premennej:
let url = 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH&tsyms=EUR';
let data = await loadData(url);
Čo ale v prípade, ak je settlement neúspešný, t.j. promise je rejectnutý (odmietnutý)? Vtedy rozbalenie výsledku znamená vyvolanie vynímky. A vynímku môžete zachytiť štandardne cez try/catch blok:
Váš kód by teda mal vyzerať skôr takto, ak by ste chceli rátať aj s prípadom, že Promise
bude odmietnutý:
console.log('start');
let url = 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH&tsyms=EUR';
try {
let data = await loadData(url)
console.log('async done');
console.log(data);
} catch(err) {
console.log('async failed');
console.error(err);
}
console.log('end');
Keď sa na tento príklad pozriete pozorne a spomeniete si na vyššie uvedený príklad, keď sme sa bavili, aké by bolo super, mať asynchrónne volanie tváriace sa ako synchrónne, tak zistíte, že príklady sú to tie isté. Teda, skoro tie isté. V našom upravenom prípade je navyše jedno slovo - await
.
Tretím efektom použitia async je pozastavenie vykonávania async
fukncie, ak sa v nej vyskytne await
.
To sme v zásade už videli, ale skúsme sa nad tým ešte zamyslieť, lebo je to v zásade niečo revolučné. Doteraz sme boli zvyknutý, že synchrónny kód beží riadok za riadkom, príkaz za príkazom a kód beží, kým neskončí.
S asynchrónnym kódom sa nám to ale zmenilo. Asynchrónnu činnosť sme vedeli spustiť synchrónne, ale nevedeli sme počkať na jej výsledok. Náš kód bežal ďalej. Spracovanie asynchrónnej činnosti išlo na rad až v momente, kedy jednak dobehlo a druhak a najmä, až keď všetok náš synchrónny kód dobehol.
async
a await
ale všetko menia. Zrazu môžete napísať funkciu, ktorú spúšťate synchrónne, v nej beží kód synchrónne až kým narazí na await
. V tom momente sa spúšťanie funkcie pozastaví a reálne čaká na dobehnutie asynchrónnej činnosti.
A sic sa to stále môže zdať ako drobnosť, je to jedna z najlepších vecí aká nás mohla postretnúť. Náš mentálny model je s týmto nie len že absolútne kompatibilný, nebuďme troškári a povedzme to na rovinu - toto je chovanie, ktoré náš mentálny model očakáva. Je to pre nás úplne prirodzené.
Týmto by som si dovolil ukončiť tento mini seriál. Pokryli sme si základy a tak by ste mali byť v tomto momente úplne komfortný s použitím a porozumením callbackov, promisov a async
/await
. A to bol cieľ.
Samozrejme o callbackoch, promisoch aj async
/await
by sa toho dalo popísať mraky, ale myslím, že nateraz ste dobre vybavený, aby ste sa s nimi cítili pohodlne, keď ich budete dennodenne používať.
Pravdepodobne sa k asynchrónnosti v budúcnosti ešte vrátim, nápadov mám množstvo, len času na písanie nie je toľko, koľko by som si predstavoval. Tak ma nezabudnite sledovať tu na blogu, alebo na twitteri...
Samozrejme, rád sa s vami (aj o asynchrónnosti) pobavím na niektorom z mojich JavaScriptových školení.
A okrem toho, že sa na nich môžete poriadne naučiť JavaScript, môžete sa pritom naučiť aj nejaký framework či knižnicu - Node.js, Angular či React.
Ak nechceš premeškať príspevky ako je tento, sleduj ma na Twitteri, alebo ak máš RSS čítačku, môžeš sledovať môj RSS kanál.