Autor članka objašnjava na primjerima Async/Await u JavaScriptu. U globalu, Async/Await je zgodan način pisanja asinkronog koda. Prije nego što je ova mogućnost postojala, takav se kod pisao korištenjem povratnih poziva i promise-a. Autor originalnog članka otkriva prednosti Async/Await, razmatrajući razne primjere.
Podsjećamo: za sve čitatelje "Habra" — popust od 10 000 rubalja prilikom upisa na bilo koji tečaj Skillbox uz promo kod "Habr".
Skillbox preporučuje: Online edukacijski tečaj .
Povratni poziv
Povratni poziv predstavlja funkciju čije se izvršenje odgađa za neodređeno vrijeme. Ranije su se povratni pozivi koristili na onim mjestima u kodu gdje rezultat nije mogao biti dobiven odmah.
Evo primjera asinkronog čitanja datoteke na Node.js:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
}); Problemi nastaju kada je potrebno izvršiti više asinkronih operacija istovremeno. Zamislimo sljedeći scenarij: šalje se zahtjev za bazu podataka korisnika Arfat, treba pročitati njegovo polje profile_img_url i učitati sliku sa servera someserver.com.
Nakon učitavanja, konvertiramo sliku u drugi format, na primjer iz PNG-a u JPEG. Ako je konverzija prošla uspješno, korisniku se šalje e-mail. Zatim se informacija o događaju zapisuje u datoteku transformations.log s oznakom datuma.
Treba obratiti pažnju na složenost povratnih poziva i veliki broj }) na kraju koda. To se naziva Callback Hell ili Pyramid of Doom.
Nedostaci ovog pristupa su očigledni:
- Ovaj kod je teško čitati.
- Također je teško obraditi greške, što često dovodi do pogoršanja kvalitete koda.
Kako bi se riješio ovaj problem, u JavaScript su dodani promise-i. Oni omogućuju zamjenu duboke ugniježdenosti povratnih poziva sa .then.
Pozitivna strana promise-a je to što se s njima kod čita mnogo bolje, od gore prema dolje, a ne s lijeva na desno. Ipak, promise-i također imaju svoje probleme:
- Potrebno je dodati veliki broj .then.
- Umjesto try/catch koristi se .catch za obradu svih grešaka.
- Rad s više promise-a unutar jednog ciklusa nije uvijek praktičan, u nekim slučajevima oni kompliciraju kod.
Evo zadatka koji će pokazati značaj posljednje točke.
Pretpostavimo da postoji for petlja koja ispisuje niz brojeva od 0 do 10 s nasumičnim intervalom (0–n sekundi). Koristeći promise, potrebno je promijeniti tu petlju tako da se brojevi ispisuju u redoslijedu od 0 do 10. Tako, ako ispis nule traje 6 sekundi, a jedinice 2 sekunde, prvo će biti ispisana nula, a zatim će početi odbrojavanje ispisivanja jedinice.
I naravno, za rješavanje ovog zadatka ne koristimo Async/Await ili .sort. Primjer rješenja – na kraju.
Async funkcije
Dodavanje async funkcija u ES2017 (ES8) pojednostavilo je rad s promissima. Napominjem da async funkcije rade "iznad" promisa. Te funkcije ne predstavljaju kvalitativno druge koncepcije. Async funkcije su zamišljene kao alternativa kodu koji koristi promise.
Async/Await omogućuje organiziranje rada s asinhronim kodom u sinkronom stilu.
Dakle, poznavanje promisea olakšava razumijevanje principa Async/Await.
Sintaksa
U običnoj situaciji sastoji se od dva ključna riječi: async i await. Prva riječ pretvara funkciju u asinhronu. U takvim funkcijama dopušteno je korištenje await. U bilo kojoj drugoj situaciji korištenje te funkcije izazvat će grešku.
// 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 umetne na samom početku deklaracije funkcije, a u slučaju korištenja streličaste funkcije – između znaka „=” i zagrada.
Te funkcije mogu biti stavljene u objekt kao metode ili se koristiti u deklaraciji klase.
// 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! Vrijedno je zapamtiti da konstruktori klase i getter/setter ne mogu biti asinhroni.
Semantika i pravila izvršenja
Async funkcije, u načelu, slične su standardnim JS funkcijama, ali postoje iznimke.
Tako, async funkcije uvijek vraćaju promise:
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloNaime, fn vraća niz hello. Budući da je to asinhrona funkcija, vrijednost niza obavija se u promise uz pomoć konstruktora.
Evo alternativne konstrukcije bez Async:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// helloU ovom slučaju vraćanje promisea vrši se "ručno". Asinhrona funkcija uvijek se obavija u novi promise.
U slučaju da je vraćena vrednost primitiv, async funkcija vraća vrednost tako što je obavija u promisa. Ako je vraćena vrednost objekat promisa, njegovo rešenje se vraća u novom promisu.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Ali šta se događa kada se unutar asinhrone funkcije desi greška?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);Ako ne bude obrađena, foo() će vratiti promet sa odbijenjem. U ovoj situaciji umesto Promise.resolve vratiće se Promise.reject koji sadrži grešku.
Async funkcije uvek daju promet kao izlaz, bez obzira na to šta se vraća.
Asinhrone funkcije se obustavljaju pri svakom await-u.
Await utiče na izraze. Ako je izraz promesa, async funkcija se obustavlja dok se ne izvrši promet. Ako izraz nije promet, konvertuje se u promet putem Promise.resolve i zatim se završava.
// 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);Evo opisa kako funkcioniše fn funkcija.
- Nakon njenog poziva, prva linija se konvertuje iz const a = await 9; u const a = await Promise.resolve(9);.
- Nakon korišćenja Await, izvršenje funkcije se obustavlja dok a ne dobije svoju vrednost (u ovom slučaju to je 9).
- delayAndGetRandom(1000) obustavlja izvršenje fn funkcije dok se ona ne završi (posle 1 sekunde). To je zapravo pauza fn funkcije od 1 sekunde.
- delayAndGetRandom(1000) preko resolve vraća slučajnu vrednost koja se zatim dodeljuje promenljivoj b.
- A situacija sa promenljivom c je slična situaciji sa promenljivom a. Nakon toga sve se obustavlja na sekundu, ali sada delayAndGetRandom(1000) ne vraća ništa jer to nije potrebno.
- Na kraju, vrednosti se računaju prema formuli a + b * c. Rezultat se obavija u promet pomoću Promise.resolve i vraća funkcijom.
Ove pauze mogu podsećati na generatore u ES6, ali za to postoje .
Rešavamo problem
A sada hajde da razmotrimo rešenje problema koji je ranije pomenut.
U funkciji finishMyTask koristi se Await za čekanje rezultata operacija poput queryDatabase, sendEmail, logTaskInFile i drugih. Kada se ovo rešenje uporedi sa onim u kojem su korišćeni ob promises, očigledna je sličnost. Ipak, verzija s Async/Await značajno pojednostavljuje sve sintaktičke složenosti. U ovom slučaju nema velikog broja povratnih poziva i lanaca poput .then/.catch.
Evo rešenja s ispisom brojeva, ovde postoje dve varijante.
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 ovo je rešenje koje koristi async funkcije.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}Obrada grešaka
Neobrađene greške se obavijaju u rejected promise. Ipak, u async funkcijama može se koristiti konstrukcija try/catch za sinhronu obradu grešaka.
async function canRejectOrReturn() {
// čekaj jednu sekundu
await new Promise(res => setTimeout(res, 1000));
// Odbij sa ~50% verovatnoće
if (Math.random() > 0.5) {
throw new Error('Žao mi je, broj je prevelik.')
}
return 'savršeni broj';
}canRejectOrReturn() je asinkrona funkcija koja se ili uspešno izvršava (‘savršeni broj’), ili ne uspeva sa greškom (‘Žao mi je, broj je prevelik’).
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'greška uhvaćena';
}
}Budući da se u gornjem primeru očekuje izvršavanje canRejectOrReturn, njeno neuspešno završavanje će pokrenuti blok catch. Kao rezultat toga, funkcija foo će se završiti ili sa undefined (kada u try bloku ništa ne bude vraćeno), ili sa greška uhvaćena. Na kraju, ova funkcija neće imati neuspešno završavanje, jer će try/catch obraditi samu funkciju foo.
Evo još jednog primera:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'greška uhvaćena';
}
}Vredno je obratiti pažnju na činjenicu da se u primeru iz foo vraća canRejectOrReturn. Foo se u ovom slučaju završava ili savršenim brojem, ili vraća grešku Error (‘Žao mi je, broj je prevelik’). Blok catch nikada neće biti izvršen.
Problem je u tome što foo vraća promise, koji je proslijeđen iz canRejectOrReturn. Stoga rešenje funkcije foo postaje rešenje za canRejectOrReturn. U ovom slučaju, kod će se sastojati od samo dve linije:
try {
const promise = canRejectOrReturn();
return promise;
}A šta će se desiti ako se zajedno koriste await i return:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'greška uhvaćena';
}
}U gornjem kodu foo će se uspješno završiti s perfect number i s error caught. Ovdje neće biti grešaka. No, foo će se završiti s canRejectOrReturn, a ne s undefined. Provjerimo to uklanjanjem retka return await canRejectOrReturn():
try {
const value = await canRejectOrReturn();
return value;
}
// …Česte greške i zamke
U nekim slučajevima korištenje Async/Await može dovesti do grešaka.
Zaboravljeni await
To se događa prilično često — ispred promise-a se zaboravi ključna riječ await:
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}U kodu, kao što se vidi, nema ni await, ni return. Stoga foo uvijek završava s undefined bez odgađanja od 1 sekunde. Međutim, promise će se izvršiti. Ako doda grešku ili se odbije, u tom će se slučaju pojaviti UnhandledPromiseRejectionWarning.
Async funkcije u povratnim pozivima
Async funkcije se često koriste unutar .map ili .filter kao callback-ovi. Primjer može biti funkcija fetchPublicReposCount(username), koja vraća broj javnih repozitorija na GitHub-u. Pretpostavimo da imamo tri korisnika čiji su podaci potrebni. Evo koda za ovu zadaću:
const url = 'https://api.github.com/users';
// Korisna funkcija za dohvaćanje broja repozitorija
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}Trebaju nam računi ArfatSalman, octocat i norvig. U tom slučaju izvršavamo:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});Obratite pažnju na Await u povratnom pozivu .map. Ovdje su counts — niz promise-a, a .map — anonimni callback za svakog navedenog korisnika.
Prekomjerno sekvencijalno korištenje await
Kao primjer uzmimo sljedeći kod:
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;
}Ovde se u varijablu count smješta broj repozitorija, zatim se ta brojka dodaje u niz counts. Problem koda je u tome što dok podaci prvog korisnika ne stignu s poslužitelja, svi ostali korisnici će biti u stanju čekanja. Tako da se u jednom trenutku obrađuje samo jedan korisnik.
Ako, na primjer, za obradu jednog korisnika treba oko 300 ms, tada za sve korisnike to već iznosi sekundu, provedeno vrijeme linearno ovisi o broju korisnika. No, budući da dobivanje broja repoa ne ovisi jedan o drugome, procese je moguće paralelizirati. Za to su potrebni .map i 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 ulazu dobiva niz promisa s vraćanjem promisa. Posljednji završava nakon što se svi promisi u nizu završe ili pri prvom odbijanju. Može se dogoditi da se svi oni ne pokrenu istovremeno — kako bi se osigurao istovremeni početak, može se koristiti p-map.
Zaključak
Async-funkcije postaju sve važnije za razvoj. Za adaptivnu upotrebu async-funkcija vrijedi koristiti . JavaScript programer treba dobro razumjeti ovo.
Skillbox preporučuje:
- Praktični tečaj .
- Prijateljski online tečaj .
- Dvoipogodišnji praktični tečaj .
Izvor: habr.com
