Artikli autor toob vĂ€lja Async/Await nĂ€ited JavaScriptis. Ăldiselt on Async/Await mugav viis asĂŒnkroonse koodi kirjutamiseks. Enne selle vĂ”imaluse ilmumist kirjutati sellist koodi tagasiside ja lubaduste abil. Algse artikli autor tutvustab Async/Await eeliseid, tuues esile mitmeid nĂ€iteid.
Tuletame meelde: kĂ”igile «Habra» lugejatele â 10 000 rubla soodustus, kui registreerite end Skillboxi mis tahes kursusele promokoodi «Habr» abil.
Skillbox soovitab: Hariduslik veebikursus .
Tagasiside
Tagasiside on funktsioon, mille kutsumine lĂŒkatakse mÀÀramata ajaks edasi. Varem kasutati tagasisideid koodilĂ”ikudes, kus tulemust ei saanud kohe saada.
Siin on nĂ€ide asĂŒnkroonsest faili lugemisest Node.js-is:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
}); Probleemid tekivad siis, kui tuleb korraga teostada mitu asĂŒnkroonset operatsiooni. Kujutame ette jĂ€rgmist stsenaariumi: vĂ”tame andmebaasist kasutaja Arfat andmeid, peame lugema tema profile_img_url vĂ€lja ja laadima pildi serverist someserver.com.
PĂ€rast ĂŒleslaadimist konverteerime pildi teise formaati, nĂ€iteks PNG-st JPEG-ks. Kui konverteerimine Ă”nnestub, saadetakse kasutaja e-posti aadressile kiri. Edasi kantakse sĂŒndmuse teave transformations.log faili koos kuupĂ€eva mĂ€rkimisega.
Oluline on tÀhele panna tagasivÔtmiste ja suure arvu }) koondumist koodi lÔpus. Seda tuntakse kui Callback Hell vÔi Pyramid of Doom.
Selle meetodi puudused on ilmne:
- Seda koodi on raske lugeda.
- Sellega on samuti keeruline vigu hallata, mis sageli viib koodi kvaliteedi halvenemiseni.
Selle probleemi lahendamiseks on JavaScripti lisatud lubadused. Need vĂ”imaldavad asendada sĂŒgava kollaaĆŸimise .then-iga.
Lubaduste positiivne kĂŒlg on see, et nendega on kood palju paremini loetav, ning see kulgeb ĂŒlespoole, mitte vasakult paremale. Siiski on lubadustel ka oma probleemid:
- Tuleb lisada palju .then.
- Try/catch'i asemel kasutatakse .catch'i kÔigi vigade kÀsitlemiseks.
- Mitme lubadusega töötamine ĂŒhe tsĂŒkli raames pole kaugeltki alati mugav, mĂ”nes olukorras muudavad nad koodi keerulisemaks.
Siin on ĂŒlesanne, mis nĂ€itab viimase punkti tĂ€hendust.
Kujutame ette for-tsĂŒklit, mis vĂ€ljastab numbrite jĂ€rjekorra vahemikus 0 kuni 10 juhuslike intervallidega (0ân sekundit). Kasutades lubadusi, tuleb see tsĂŒkkel muuta nii, et numbrid vĂ€ljastatakse jĂ€rjestikku 0-st 10-ni. NĂ€iteks, kui nulli vĂ€ljastamine kestab 6 sekundit ja ĂŒhele 2 sekundit, siis peaks esmalt olema vĂ€ljastatud null, enne kui ĂŒhe vĂ€ljastamine algab.
Ja loomulikult ei kasuta me selle ĂŒlesande lahendamiseks Async/Await ega .sort. NĂ€ide lahendusest on lĂ”pus.
Async-funktsioonid
Async-funktsioonide lisamine ES2017 (ES8) lihtsustas lubadustega töötamise ĂŒlesannet. Tahan rĂ”hutada, et async-funktsioonid töötavad "lubaduste" peal. Need funktsioonid ei esinda kvaliteetselt teistsuguseid kontseptsioone. Async-funktsioonid mĂ”eldi vĂ€lja alternatiivina koodile, mis kasutab lubadusi.
Async/Await annab vĂ”imaluse korraldada asĂŒnkroonse koodi töötlemist sĂŒnkroonse stiili jĂ€rgi.
Seega, lubaduste tundmine lihtsustab Async/Await pÔhimÔtete mÔistmist.
SĂŒntaks
Tavaliselt koosneb see kahest vĂ”tmesĂ”nast: async ja await. Esimene sĂ”na muudab funktsiooni asĂŒnkroonseks. Sellistes funktsioonides on await'i kasutamine lubatud. Igas muus olukorras nende funktsioonide kasutamine tekitab vea.
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
Async lisatakse kÔigepealt funktsiooni deklareeringusse ja kui kasutatakse noolfunktsiooni, siis '=' ja sulgude vahele.
Need funktsioonid saab paigutada objekti meetoditena vÔi kasutada klassi deklareerimisel.
// 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! Tuleb meeles pidada, et klasside konstruktorid ja getter'id/setter'id ei saa olla asĂŒnkoolsed.
Semantika ja tÀitmisreeglid
Async-funktsioonid on pÔhimÔtteliselt sarnased tavalistele JS-funktsioonidele, kuid erandeid on.
Nii et async-funktsioonid tagastavad alati lubadusi:
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloKonkreetsemalt tagastab fn stringi hello. Kuna see on asĂŒnkroonne funktsioon, siis see stringi vÀÀrtus mĂ€hitakse lubadusse konstruktori abil.
Siin on alternatiivne konstruktsioon ilma Async-ta:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// helloSellisel juhul toimub lubaduse tagastamine âkĂ€tegaâ. AsĂŒnkroonset funktsiooni mĂ€hitakse alati uude lubadusse.
Kui tagastatav vÀÀrtus on primitiivne, siis async-funktsioon tagastab vÀÀrtuse, ĂŒmber pakkides selle lubadusse. Kui tagastatav vÀÀrtus on lubaduse objekt, siis selle lahendus tagastatakse uues lubaduses.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Kuid mis juhtub, kui async-funktsiooni sees tekib viga?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);Kui viga ei kÀsitleta, siis foo() tagastab lubaduse, millel on reject. Sel juhul tagastatakse Promise.resolve asemel Promise.reject koos veaga.
Async-funktsioonid tagastavad alati lubaduse, olenemata sellest, mis tagastatakse.
AsĂŒnkroonsed funktsioonid peatuvad iga await'i korral.
Await mÔjutab vÀljendeid. Kui vÀljend on lubadus, peatub async-funktsioon lubaduse tÀitmiseni. Kui vÀljend ei ole lubadus, konverteeritakse see lubaduseks lÀbi Promise.resolve ja seejÀrel lÔpetatakse.
// 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);Siin on kirjeldus sellest, kuidas fn-funktsioon töötab.
- PÀrast selle vÀljakutset konverteeritakse esimene rida const a = await 9; argumendiks const a = await Promise.resolve(9);.
- PÀrast Await kasutamist peatub funktsiooni tÀitmine, kuni see ei saa oma vÀÀrtust (kÀesoleval juhul 9).
- delayAndGetRandom(1000) peatab fn-funktsiooni tÀitmise, kuni see ise lÔpetatakse (pÀrast 1 sekundit). See on sisuliselt fn-funktsiooni peatamine 1 sekundiks.
- delayAndGetRandom(1000) tagastab resolve'i kaudu juhusliku vÀÀrtuse, mis seejÀrel antakse muutujale b.
- Ja muutuja c juhtum on sarnane muutuja a juhtumile. PĂ€rast seda peatuvad kĂ”ik sekundiks, kuid nĂŒĂŒd delayAndGetRandom(1000) ei tagasta midagi, kuna see pole vajalik.
- Seega arvutatakse vÀÀrtused valemi jÀrgi a + b * c. Tulemuseks on lubaduse sisestamine Promise.resolve abil ja selle tagastamine funktsiooni kaudu.
Need pausid vÔivad meenutada loodreid ES6-s, kuid sellele on .
Lahendame ĂŒlesande.
NĂŒĂŒd vaatame lahendust ĂŒlesandele, mis oli eespool mainitud.
Funktsioonis finishMyTask kasutatakse Await, et oodata selliste operatsioonide nagu queryDatabase, sendEmail, logTaskInFile tulemusi. Kui vĂ”rrelda seda lahendust lubadega, on sarnasused ilmsed. Siiski, Async/Await versioon lihtsustab oluliselt kĂ”iki sĂŒntaktilisi keerukusi. Sellisel juhul pole palju tagasiside funktsioone ja ahelaid nagu .then/.catch.
Siin on lahendus numbrite vÀljastamiseks, kus on kaks varianti.
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);
});
});
};Ja siin on lahendus, mis kasutab async-funktsioone.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}Vigade kÀsitsemine
Töötlemata vead ĂŒmbritsetakse tagasi lĂŒkatud lubadusse. Siiski saavad async-funktsioonides kasutada try/catch konstruktsiooni, et teostada sĂŒnkroonne veahaldus.
async function canRejectOrReturn() {
// oota ĂŒks sekund
await new Promise(res => setTimeout(res, 1000));
// tagasi lĂŒkata ~50% tĂ”enĂ€osusega
if (Math.random() > 0.5) {
throw new Error('Kahjuks on number liiga suur.')
}
return 'tÀiuslik number';
}canRejectOrReturn() on asĂŒnkroonne funktsioon, mis kas tĂ€idetakse edukalt ("tĂ€iuslik number") vĂ”i see lĂ”ppeb veaga ("Kahjuks on number liiga suur").
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'viga pĂŒĂŒti';
}
}Kuna ĂŒlaltoodud nĂ€ites oodatakse canRejectOrReturn'i tĂ€itmist, siis oma ebaĂ”nnestumine toob kaasa catch-bloki tĂ€itmise. Selle tulemusena lĂ”peb funktsioon foo kas undefined'iga (kui try-blokis midagi ei tagastata) vĂ”i error caught'iga. Selle tulemusena ei saa selle funktsiooni lĂ”ppeda ebaĂ”nnestumisega, kuna try/catch tegeleb ise funktsiooni foo töötlemisega.
Siin on veel ĂŒks nĂ€ide:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}Oluline on tĂ€helepanu pöörata sellele, et foo nĂ€ites tagastatakse canRejectOrReturn. Foo lĂ”peb sel juhul kas perfect number'iga vĂ”i tagastab vea Error (âKahjuks, number liiga suurâ). Catch-blokki kunagi ei tĂ€ideta.
Probleem on selles, et foo tagastab lubaduse, mis on edastatud canRejectOrReturn'ilt. SeetÔttu muutub funktsiooni foo lahendus canRejectOrReturn'i lahenduseks. Sellisel juhul koosneb kood vaid kahest read:
try {
const promise = canRejectOrReturn();
return promise;
}Kuid vaata, mis juhtub, kui kasutada koos await'i ja return'i:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}Ălaltoodud koodis lĂ”petab foo edukalt nii tĂ€iusliku numbri kui ka kinni pĂŒĂŒtud veaga. Siin ei ole ebaĂ”nnestumisi. Kuid foo lĂ”petab canRejectOrReturn'i, mitte undefined'iga. Veendugem selles, eemaldades rea return await canRejectOrReturn():
proovige {
const value = await canRejectOrReturn();
return value;
}
// âŠLevinud vead ja takistused
MÔnel juhul vÔib Async/Await'i kasutamine viia vigadeni.
Unustatud await
See juhtub piisavalt tihti â enne lubadust unustatakse vĂ”tmesĂ”na await:
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}Koodis, nagu nÀha, ei ole ei await'i ega return'i. SeetÔttu lÔpetab foo alati undefined'iga ilma 1-sekundilise viivituseta. Kuid lubadus tÀidetakse. Kui aga see toob kaasa vea vÔi reject'i, siis sellisel juhul kutsutakse esile UnhandledPromiseRejectionWarning.
Async-funktsioonid callback'ides
Async-funktsioone kasutatakse ĂŒsna sageli .map vĂ”i .filter callbĂ€ck'ideks. NĂ€iteks vĂ”ib tuua funktsiooni fetchPublicReposCount(username), mis naaseb avatud GitHubi reposiitide arvu. Oletame, et meil on kolm kasutajat, kelle nĂ€itajad on vajalikud. Siin on kood selle ĂŒlesande jaoks:
const url = 'https://api.github.com/users';
// Kasutame funktsiooni, et hankida repo arvu
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}Me vajame kontosid ArfatSalman, octocat, norvig. Sellisel juhul teeme jÀrgmist:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});Oluline on mĂ€rkida Await'i kasutamist .map tagasi kutsumises. Siin on counts â massiiv lubadustest, ning .map on anonĂŒĂŒmne tagasiside iga nimega kasutaja kohta.
Liigne jÀrgneva await'i kasutamine
VÔtame nÀiteks jÀrgmise koodi:
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;
}Siin salvestatakse muutuja count'i rea repo arvu, seejĂ€rel lisatakse arv massiivi counts. Probleem seisneb selles, et kuni esimese kasutaja andmed serverist ei tule, on kĂ”ik jĂ€rgnevad kasutajad ooteolukorras. Seega töödeldakse korraga vaid ĂŒhte kasutajat.
Kui ĂŒhe kasutaja töötlemiseks kulub umbes 300 ms, siis kĂ”igi kasutajate puhul on see juba sekund, kulutatud aeg sĂ”ltub lineaarselt kasutajate arvust. Kuid kuna repode arv ei sĂ”ltu ĂŒksteisest, saab protsesse paralleelselt kĂ€ivitada. Selleks on vajalik .map ja Promise.all kasutamine:
async function fetchAllCounts(users) {
const promises = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
return Promise.all(promises);
}Promise.all saab sisendiks massiivi lubadusi, tagastades lubaduse. Viimane lĂ”peb pĂ€rast kĂ”igi massiivis olevate lubaduste tĂ€itmist vĂ”i esimese tagasilĂŒkkamise korral. VĂ”ib juhtuda, et need kĂ”ik ei kĂ€ivitunud korraga â samaaegse kĂ€ivituse tagamiseks vĂ”ib kasutada p-map.
KokkuvÔte
Async-funktsioonid muutuvad arenduses ĂŒha olulisemaks. Ja et kasutada async-funktsioone tĂ”husalt, on mĂ”istlik kasutada . JavaScripti arendaja peab sellest hĂ€sti aru saama.
Skillbox soovitab:
- Praktiline kursus .
- Rakenduslik veebikursus .
- Kaheaastane praktiline kursus .
Allikas: habr.com
