Artikli autor kĂ€sitleb nĂ€idete abil Async/Await'i JavaScriptis. Ăldiselt on Async/Await mugav viis asĂŒnkroonse koodi kirjutamiseks. Enne selle vĂ”imaluse tekkimist kirjutati selline kood tagasitulekute ja lubaduste abil. Algse artikli autor toob vĂ€lja Async/Await'i eelised, kĂ€sitledes erinevaid nĂ€iteid.
Tuletame meelde: kĂ”igile «Habr» lugejatele â 10 000 rubla allahindlus igale Skillboxi kursusele, kasutades sooduskoodi «Habr».
Skillbox soovitab: Haridusalane veebikursus .
Tagasitulek
Tagasitulek on funktsioon, mille kutsumine on edasi lĂŒkatud mÀÀramata ajaks. Varem kasutati tagasitulekute vĂ”imalust koodikohtades, kus tulemust ei saanud kohe saada.
Siin on nĂ€ide asĂŒnkroonsest failist lugemisest Node.js-is:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
}); Probleemid tekivad hetkel, kui tuleb teha mitu asĂŒnkroonset operatsiooni korraga. Kujutame ette jĂ€rgmist stsenaariumi: teostatakse pĂ€ring kasutaja Arfati andmebaasi, tuleb lugeda tema profiilipildi vĂ€ljatĂ”mbamise url ja laadida pilt serverist someserver.com.
PĂ€rast pildi laadimist konverteerime selle teise vormingusse, nĂ€iteks PNG-st JPEG-ks. Kui konverteerimine Ă”nnestub, saadetakse kasutaja e-posti aadressile kiri. Edasi kantakse teave sĂŒndmuse kohta faili transformations.log koos kuupĂ€evaga.
Tasub tĂ€helepanu pöörata tagasitulekute ĂŒksteise peale ladumisele ja suurele hulgale }) koodi lĂ”pus. Seda nimetatakse Callback Hell'iks vĂ”i Pyramid of Doom'iks.
Selle meetodi puudused on ilmne:
- Seda koodi on keeruline lugeda.
- Samuti on selles keeruline vigu kÀsitleda, mis sageli viib koodi kvaliteedi halvenemiseni.
Selle probleemi lahendamiseks lisati JavaScripti lubadused. Need vĂ”imaldavad asendada sĂŒgava tagasitulekute pesastatud struktuuri sĂ”naga .then.
Lubaduste positiivne kĂŒlg on see, et nende abil on kood palju paremini loetav, liigeldes ĂŒlevalt alla, mitte vasakult paremale. Siiski on lubadustel ka oma probleemid:
- Tuleb lisada suur hulk .then-e.
- Vigade kÀsitlemiseks kasutatakse .catch'i, mitte try/catch'i.
- Mitme lubadusega töötamine ĂŒhes tsĂŒklis ei ole kaugeltki alati mugav, mĂ”nel juhul keerukad nad koodi.
Siin on ĂŒlesanne, mis nĂ€itab viimase punkti olulisust.
Oletame, et on tsĂŒkkel for, mis vĂ€ljastab numbrite jada 0 kuni 10 juhusliku vahega (0ân sekundit). Kasutades lubadusi, tuleb see tsĂŒkkel muuta nii, et numbrid vĂ€ljastataks jĂ€rjestikku 0 kuni 10. Seega, kui nulli vĂ€ljastamine kestab 6 sekundit ja ĂŒhekĂŒmnendat 2 sekundit, siis peaks esmalt olema vĂ€ljastatud null ja seejĂ€rel peaks hakkama ĂŒhe vĂ€ljastamine.
Ja muidugi, selle ĂŒlesande lahendamiseks me ei kasuta Async/Await ega .sort. Lahenduse nĂ€ide on lĂ”pus.
Async-funktsioonid
Async-funktsioonide lisamine ES2017 (ES8) lihtsustas lubadustega töötamist. Tahan mÀrkida, et async-funktsioonid töötavad "lubaduste peal". Need funktsioonid ei kujuta endast kvaliteedi poolest teisi kontseptsioone. Async-funktsioonid mÔeldi alternatiivina koodile, mis kasutab lubadusi.
Async/Await annab vĂ”imaluse korraldada asĂŒnkroonse koodi töötlemine sĂŒnkrone stiilis.
Seega, lubaduste tundmine lihtsustab Async/Await mÔistmise pÔhimÔtteid.
SĂŒntaks
Tavaliselt koosneb see kahest peamistest sĂ”nast: async ja await. Esimene sĂ”na muudab funktsiooni asĂŒnkroonseks. Sellistes funktsioonides on lubatud kasutada await. Igas teises juhus jĂ€tab selle funktsiooni kasutamine 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 paigutatakse kohe funktsiooni deklareeringu algusesse ning juhul, kui kasutatakse noolfunktsiooni â mĂ€rgi "=" ja sulgude vahele.
Need funktsioonid vÔib paigutada objektidesse 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 klassi konstruktorid ning get- ja set-funktsioonid ei saa olla asĂŒnkroonilised.
Semantika ja kÀitamise reeglid
Async-funktsioonid on pÔhimÔtteliselt sarnased tavalistele JS-funktsioonidele, kuid on ka erandeid.
Nii et async-funktsioonid tagastavad alati lubadusi:
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloEriti, fn tagastab stringi hello. Kuna see on asĂŒnkrooniline funktsioon, siis vÀÀrtus stringist on mĂ€hitud lubadusse konstruktoriga.
Siin on alternatiivne konstruktsioon ilma Async-ta:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// helloSel juhul toimub lubaduse tagastamine "kĂ€sitsi". AsĂŒnkrooniline funktsioon on alati mĂ€hitud uude lubadusse.
Kui tagastatav vÀÀrtus on primitiivne, siis async-funktsioon tagastab vÀÀrtuse, mÀhkides selle lubadusse. Kui tagastatav vÀÀrtus on aga lubaduse objekt, siis selle lahendamine tagastatakse uues lubaduses.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Kuid mis juhtub, kui asĂŒnkroonses funktsioonis tekib viga?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);Kui viga ei kĂ€sitleta, tagastab foo() lubaduse, millel on tagasilĂŒkkamine. Sellisel juhul tagastatakse Promise.resolve asemel Promise.reject, mis sisaldab viga.
AsĂŒnkroonsed funktsioonid tagastavad alati lubaduse, olenemata sellest, mida tagastatakse.
AsĂŒnkroonsed funktsioonid peatatakse iga await'i korral.
Await mÔjutab vÀljendeid. Seega, kui vÀljend on lubadus, peatub async-funktsioon kuni lubaduse tÀitmiseni. Kui vÀljend ei ole lubadus, muudetakse see lubaduseks lÀbi Promise.resolve ja siis 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 kir ĐŸĐżĐžŃĐ°ĐœĐžĐ”, kuidas fn-funktsioon töötab.
- PĂ€rast selle kutset muudetakse esimene rida const a = await 9; kujule const a = await Promise.resolve(9);.
- PÀrast Await'i kasutamist peatub funktsiooni tÀitmine, kuni a saab oma vÀÀrtuse (antud juhul on see 9).
- delayAndGetRandom(1000) peatab fn-funktsiooni tÀitmise, kuni see ise lÔpeb (pÀrast 1 sekundit). See on tegelikult fn-funktsiooni peatamine 1 sekundi jooksul.
- delayAndGetRandom(1000) tagastab resolve'i kaudu juhusliku vÀÀrtuse, mis siis mÀÀratakse muutuja b.
- Muutuja c juhtum on sarnane muutuja a juhtumile. PÀrast seda peatub kÔik sekundiks, kuid seekord delayAndGetRandom(1000) ei tagasta midagi, kuna seda ei ole vajadus.
- LÔpuks loetakse vÀÀrtused valemi a + b * c kaudu. Tulemuseks mÀhitakse lubadusse lÀbi Promise.resolve ja tagastatakse funktsioonist.
Need pausid vÔivad meenutada generaatoritele ES6-s, kuid sellel on .
Lahendame ĂŒlesande
NĂŒĂŒd vaatame ĂŒlesande lahendust, mis oli eelnevalt kirjeldatud.
Funktsioonis finishMyTask kasutatakse Await'i, et oodata selliste toimingute tulemusi nagu queryDatabase, sendEmail, logTaskInFile ja teised. Kui vĂ”rrelda seda lahendust lubade kasutamisega, muutub sarnasus selgeks. Siiski muudab Async/Await'i versioon oluliselt lihtsamaks kogu sĂŒntaktilise keerukuse. Sellisel juhul pole palju tagasisidefunktsioone ja ahelaid, nagu .then/.catch.
Siin on lahendus numbrite vÀljundiga, 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);
});
});
};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Àsitlemine
KĂ€sitlemata vead pakitakse rejected lubadusse. Siiski vĂ”ib async-funktsioonides kasutada try/catch konstruktsiooni, et teha sĂŒntaktilist vigade kĂ€sitlemist.
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Ă€idab edukalt (âtĂ€iuslik numberâ) vĂ”i ebaĂ”nnestub veaga (âKahjuks on number liiga suurâ).
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'viga tabatud';
}
}Kuna eespool mainitud nÀites oodatakse canRejectOrReturn'i tÀitmist, tÔukab enda ebaÔnnestumine kaasa catch ploki tÀitmise. Selle tulemusena lÔppeb funktsioon foo kas undefined'iga (kui try plokis ei tagastata midagi) vÔi 'viga tabatud'. Seega ei ole selle funktsiooni ebaÔnnestumist, kuna try/catch peaks tegelema funktsiooni foo enda vigadega.
Siin on veel ĂŒks nĂ€ide:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'viga tabatud';
}
}On oluline mĂ€rkida, et nĂ€ites tagastatakse foo's canRejectOrReturn. Foo lĂ”ppeb sel juhul kas 'tĂ€iuslik number' vĂ”i tagastatakse viga Error (âKahjuks on number liiga suurâ). Catch plokk ei pruugi kunagi tĂ€ituda.
Probleem on see, et foo tagastab lubaduse, mis on saadud canRejectOrReturn'ilt. Seega muutub funktsiooni foo lahendus canRejectOrReturn'i lahenduseks. Sel juhul koosneb kood vaid kahest reast:
try {
const promise = canRejectOrReturn();
return promise;
}Ja nĂŒĂŒd, mis juhtub, kui kasutada koos await'i ja return'i:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'viga tabatud';
}
}Ălaltoodud koodis lĂ”petab foo edukalt nii perfect number kui error caught. Siin ei ole tĂ”rkeid. Kuid foo lĂ”petab canRejectOrReturniga, mitte undefinediga. Veendume selles, eemaldades rea return await canRejectOrReturn():
try {
const value = await canRejectOrReturn();
return value;
}
// âŠLevinud vead ja karid
MÔnel juhul vÔib Async/Awaiti kasutamine kaasa tuua vigu.
Unustatud await
Selline asi juhtub ĂŒsna tihti â enne lubadust unustatakse vĂ”tmesĂ”na await:
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}Koodis, nagu on nÀhtav, ei ole ei awaiti ega returni. SeetÔttu lÔpetab foo alati undefinediga ilma 1 sekundi viivituseta. Kuid lubadus tÀidetakse. Kui see aga annab vea vÔi tÔrke, siis kutsutakse vÀlja UnhandledPromiseRejectionWarning.
Async-funktsioonid tagasisidetes
Async-funktsioone kasutatakse ĂŒsna sageli .map vĂ”i .filter tagasisidena. NĂ€iteks vĂ”ib tuua funktsiooni fetchPublicReposCount(username), mis tagastab GitHubis avatud repode arvu. Oletame, et meil on kolm kasutajat, kelle kohta me need andmed vajame. Siin on kood selle ĂŒlesande tĂ€itmiseks:
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'];
}Me vajame ArfatSalman, octocat, norvigi kontosid. Sellisel juhul teeme jÀrgmist:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});Tuleb mĂ€rkida Awaiti kasutamist .map tagasisides. Siin on counts - lubaduste massiiv, ja .map - anonĂŒĂŒmne tagasiside iga nimetatud kasutaja jaoks.
Liigne jÀrjestikune awaiti kasutamine
NÀiteks vaadake jÀrgmist 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 pannakse muutuja counti arvu repo, seejĂ€rel lisatakse see arv massiivi counts. Koodi probleem on selles, et seni, kuni esimese kasutaja andmed serverist ei tule, ootavad kĂ”ik jĂ€rgmised kasutajad. Seega töödeldakse ĂŒhel ajal vaid ĂŒht kasutajat.
Kui nĂ€iteks ĂŒhe kasutaja töötlemiseks kulub umbes 300 ms, siis kĂ”igi kasutajate jaoks on see juba sekund, kulutatud aeg sĂ”ltub samuti lineaarselt kasutajate arvust. Kuid kuna repo arvu saamine ei sĂ”ltu teineteisest, saab protsesse paralleelseteks muuta. Selleks on vajalik töötamine .map ja Promise.all-iga:
async function fetchAllCounts(users) {
const promises = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
return Promise.all(promises);
}Promise.all vĂ”tab sisendis vastu massiivi lubadusi, mis tagastab lubaduse. Viimane lĂ”petatakse pĂ€rast kĂ”igi massiivis olevate lubaduste tĂ€itmist vĂ”i esimese tagasilĂŒkkamise korral. VĂ”ib juhtuda, et kĂ”ik need ei kĂ€ivitata samal ajal, â selleks, et tagada samaaegne kĂ€itamine, saab kasutada p-map'i.
KokkuvÔte
Async-funktsioonid muutuvad arenduses ĂŒha olulisemaks. Ja optimaalseteks async-funktsioonide kasutamiseks tasub kasutada . JavaScripti arendaja peab selles hĂ€sti orienteeruma.
Skillbox soovitab:
- Praktiline kursus .
- Rakenduslik veebikursus .
- Kaks aastat praktilist kursust .
Allikas: habr.com
