Autori i artikullit shqyrton shembujt e Async/Await në JavaScript. Në përgjithësi, Async/Await është një mënyrë e përshtatshme për të shkruar kod asinkron. Para se të shfaqej kjo mundësi, kodet e tilla shkruheshin duke përdorur callback-e dhe premisa. Autori i artikullit origjinal zbulon avantazhet e Async/Await duke shqyrtuar shembuj të ndryshëm.
Kujtojmë: për të gjithë lexuesit e «Habra» — zbritje prej 10,000 rublesh për regjistrimin në çdo kurs Skillbox me kodin promovues «Habr».
Skillbox rekomandon: Kurs online edukativ .
Callback
Callback është një funksion, thirrja e të cilit është e shtyrë për një kohë të pacaktuar. Më parë, callback-ët përdorej në ato pjesë të kodit ku rezultati nuk mund të merrej menjëherë.
Ja një shembull i leximit asinkron të një skedari në Node.js:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
}); Problemet shfaqen atëherë kur duhen kryer disa operacione asinkrone njëherësh. Le të imagjinojmë një skenar të tillë: bëhet një kërkesë në DB për përdoruesin Arfat, duhet të lexojmë fushën e tij profile_img_url dhe të ngarkojmë imazhin nga serveri someserver.com.
Pas ngarkimit, ne e konvertojmë imazhin në një format tjetër, për shembull nga PNG në JPEG. Nëse konvertimi kalon me sukses, përdoruesit dërgohet një email. Më pas, informacioni mbi ngjarjen regjistrohet në skedarin transformations.log me datën e shënuar.
Duhet t'i kushtojmë vëmendje mbivendosjes së callback-eve dhe numrit të madh të }) në pjesën përfundimtare të kodit. Kjo quhet Callback Hell ose Pyramid of Doom.
Disavantazhet e këtij mënyre janë të dukshme:
- Ky kod është i vështirë për t'u lexuar.
- Në të është gjithashtu e vështirë të trajtohen gabimet, çka shpesh çon në përkeqësimin e cilësisë së kodit.
Për të zgjidhur këtë problem, JavaScript ka shtuar premisat. Ato lejojnë zëvendësimin e thellësisë së callback-eve me fjalën .then.
Një moment pozitiv i premisave është se me to kodi lexohen shumë më mirë, nga lartë poshtë, e jo nga majtas djathtas. Megjithatë, premisat gjithashtu kanë problemet e tyre:
- Duhet të shtohet një numër i madh .then.
- Në vend të try/catch, përdoret .catch për të trajtuar të gjitha gabimet.
- Puna me disa premisa brenda një cikli nuk është gjithmonë e përshtatshme, në disa raste ato e komplikuar kodin.
Ja një detyrë, e cila do të tregojë rëndësinë e pikës fundit.
Le të supozojmë se ka një cikël for që printon një sërë numrash nga 0 në 10 me një interval të rastësishëm (0–n sekonda). Duke përdorur premtimet, duhet ta ndryshoni këtë cikël në mënyrë që numrat të shtypen në rendin nga 0 në 10. Pra, nëse printimi i zeros zgjat 6 sekonda dhe ai i njës, 2 sekonda, duhet të printohet fillimisht zero dhe pastaj të fillojë numërimi i printimit të njësit.
Dhe sigurisht, për të zgjidhur këtë detyrë nuk përdorim Async/Await ose .sort. Një shembull zgjidhjeje është në fund.
Async-funksionet
Shtimi i funksioneve async në ES2017 (ES8) e lehtësoi punën me premtimet. Dua të theksoj se funksionet async punojnë "në sipërfaqe" të premtimeve. Këto funksione nuk paraqesin koncepte cilësisht të ndryshme. Funksionet async janë menduar si një alternativë për kodin që përdor premtimet.
Async/Await ofron mundësinë për të organizuar punën me kodin asinkron në stilin sinkron.
Pra, njohja e premtimeve lehtëson kuptimin e parimeve të Async/Await.
Sintaksa
Në një situatë normale, ai përbëhet nga dy fjalë kyçe: async dhe await. Fjala e parë e kthen funksionin në asinkron. Në këto funksione, përdorimi i await lejohet. Në çdo rast tjetër, përdorimi i kësaj funksioni do të shkaktojë një gabim.
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
Async vendoset në fillim të shpalljes së funksionit, dhe në rastin e përdorimit të funksionit të arrow, midis shenjës "=" dhe parantezave.
Këto funksione mund të vendosen në një objekt si metoda ose të përdoren në shpalljen e një 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! Duhet të mbani mend se konstruktoret e klasës dhe getter/setter nuk mund të jenë asinkronë.
Semantika dhe rregullat e ekzekutimit
Async-funksionet, në thelb, janë të ngjashme me funksionet standarde JS, por ka përjashtime.
Pra, funksionet async gjithmonë kthejnë premtimet:
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloNë veçanti, fn kthen stringun hello. Ndërsa kjo është një funksion asinkron, vlera e strings mbështjellët në një premtim duke përdorur konstruktorin.
Ja një konstrukcion alternativ pa Async:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// helloNë këtë rast, kthimi i premtimit bëhet "me dorë". Funksioni asinkron gjithmonë mbështillet në një premtim të ri.
Në rastin kur vlera e kthyer është një primitiv, funksioni async kthen vlerën, duke e mbështjellë atë në një premisë. Në rastin kur vlera e kthyer është një objekt premisë, zgjidhja e tij kthehet në një premisë të re.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Por çfarë do të ndodhë nëse brenda funksionit asinkron ndodhet një gabim?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);Nëse nuk përpilohet, foo() do të kthejë një premisë me refuzim. Në këtë situatë, në vend të Promise.resolve do të kthehet Promise.reject, që përmban gabimin.
Funksionet asinkrone gjithmonë japin një premisë në daljen e tyre, pavarësisht nga ajo që kthehet.
Funksionet asinkrone pezullohen me çdo await.
Await ndikon në shprehjet. Pra, nëse shprehja është një premisë, funksioni async pezullohet deri sa premisë të ekzekutohet. Në rastin kur shprehja nuk është një premisë, ajo konvertohet në një premisë përmes Promise.resolve dhe më pas përfundon.
// 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);Këtu është përshkrimi se si funksionon funksioni fn.
- Pasi të thirret, rreshti i parë konvertohet nga const a = await 9; në const a = await Promise.resolve(9);.
- Pasi të përdoret Await, ekzekutimi i funksionit pezullohet derisa a të marrë vlerën e tij (në këtë situatë është 9).
- delayAndGetRandom(1000) pezullohet ekzekutimi i funksionit fn, derisa vetë të përfundojë (pas 1 sec). Kjo faktikisht është një ndalim i funksionit fn për 1 sekondë.
- delayAndGetRandom(1000) përmes resolve kthen një vlerë të rastësishme, e cila më pas i jepet variablës b.
- Ndërsa rasti me variablën c është i ngjashëm me atë të variablës a. Pas kësaj, gjithçka ndalet për një sekondë, por tani delayAndGetRandom(1000) nuk kthën asgjë, pasi kjo nuk nevojitet.
- Si rezultat, vlerat llogariten sipas formulës a + b * c. Rezultati më pas mbështjellë në premisë përmes Promise.resolve dhe kthehet nga funksioni.
Këto pauza mund të duken si gjeneruesit në ES6, por ka .
Zgjidhim problemin
Tani le të shohim zgjidhjen e problemit që u përmend më sipër.
Në funksionin finishMyTask, përdoret Await për të pritur rezultatet e operacioneve si queryDatabase, sendEmail, logTaskInFile dhe të tjera. Po të krahasosh këtë zgjidhje me atë, ku janë përdorur premtimet, ngjashmëria është e dukshme. Megjithatë, versioni me Async/Await e thjeshton shumë komplekse të sintaksës. Në këtë rast, nuk ka një numër të madh kolbekësh dhe zinxhirësh si .then/.catch.
Ja zgjidhja me printimin e numrave, këtu ka dy variante.
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 zgjidhja duke përdorur funksionet async.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}Trajtimi i gabimeve
Gabimet e papërpunuara rrethohen nga një premtim i refuzuar. Megjithatë, në funksionet async mund të përdoret konstrukti try/catch për të kryer një trajtim sinkron të gabimeve.
async function canRejectOrReturn() {
// prisni një sekondë
await new Promise(res => setTimeout(res, 1000));
// Refuzoni me ~50% probabilitet
if (Math.random() > 0.5) {
throw new Error('Na vjen keq, numri është shumë i madh.')
}
return 'numri perfekt';
}canRejectOrReturn() është një funksion asinkron që ose ekzekutohet me sukses (“numri perfekt”), ose përfundon me dështim me një gabim (“Na vjen keq, numri është shumë i madh”).
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'gabimi u kap';
}
}Duke qenë se në shembullin e mësipërm pritet ekzekutimi i canRejectOrReturn, një përfundim i dështuar do të rezultojë në ekzekutimin e bllokut catch. Si rezultat, funksioni foo do të përfundojë ose me undefined (kur nuk kthehet asgjë në bllokun try), ose me gabimi u kap. Në fund, ky funksion nuk do të ketë një përfundim të dështuar, pasi try/catch do të merret me procesin e funksionit foo.
Ja një shembull tjetër:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'gabimi u kap';
}
}Duhet të theksohet se në shembullin nga foo kthehet canRejectOrReturn. Foo në këtë rast përfundon ose me numrin perfekt, ose kthehet me gabimin Error (“Na vjen keq, numri është shumë i madh”). Blloku catch kurrë nuk do të ekzekutohet.
Problemi është se foo kthen një premtim, të cilin e merr nga canRejectOrReturn. Pra, zgjidhja e funksionit foo bëhet zgjidhje për canRejectOrReturn. Në këtë rast, kodi do të përbëhet vetëm nga dy rreshta:
try {
const promise = canRejectOrReturn();
return promise;
}Ja çfarë do të ndodhte, nëse përdoren së bashku await dhe return:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'gabimi u kap';
}
}Në kodin më sipër, foo do të përfundojë me sukses si me numrin perfekt ashtu edhe me gabimin e kapur. Nuk do të ketë ndalesa këtu. Por foo do të përfundojë me canRejectOrReturn, dhe jo me undefined. Le të sigurohemi për këtë duke hequr rreshtin return await canRejectOrReturn():
provoni {
const value = await canRejectOrReturn();
return value;
}
// …Gabimet e zakonshme dhe pengesat
Në disa raste, përdorimi i Async/Await mund të çojë në gabime.
E haruar await
Kjo ndodh mjaft shpesh — para premtimit harrohet fjala kyçe await:
async function foo() {
provoni {
canRejectOrReturn();
} kapni (e) {
return 'kapur';
}
}Në kod, siç dallohet, nuk ka as await, as return. Prandaj foo gjithmonë përfundon me undefined pa një vonesë prej 1 sekonde. Por premtimi do të ekzekutohet. Nëse ai jep një gabim ose refuzim, atëherë në këtë rast do të thirret UnhandledPromiseRejectionWarning.
Funksionet Async në thirrjet e kthimit
Funksionet Async përdoren mjaft shpesh në .map ose .filter si thirrje kthimi. Si një shembull, mund të përmendim funksionin fetchPublicReposCount(username), i cili kthen numrin e repozitoreve të hapura në GitHub. Supozoni se ka tre përdoruesish, për të cilët na nevojiten statistikat. Ja kodi për këtë detyrë:
const url = 'https://api.github.com/users';
// Funksioni utilitar për të marrë numrat e repozitorëve
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}Na duhen llogaritë ArfatSalman, octocat, norvig. Në këtë rast e ekzekutojmë:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});Duhet të vihet re Await në thirrjen kthyes .map. Këtu counts është një masë premtimesh, ndënj një thirrje anonime për çdo përdorues të cituar.
Përdorimi i tepruar i await në mënyrë radhazi
Si një shembull, le të marrim këtë kod:
async function fetchAllCounts(users) {
const counts = [];
për (let i = 0; i < users.length; i++) {
const username = users[i];
const count = await fetchPublicReposCount(username);
counts.push(count);
}
return counts;
}Këtu në variablën count vendoset numri i repozitorëve, pastaj ky numër shtohet në masën counts. Problemi me kodin është se derisa të vijnë të dhënat për përdoruesin e parë nga serveri, të gjithë përdoruesit e tjerë do të ngelin në pritje. Prandaj, në një moment të vetëm përpunohet vetëm një përdorues.
Për shembull, nëse për përpunimin e një përdoruesi nevojiten rreth 300 ms, për të gjithë përdoruesit kjo tashmë është një sekondë; koha e shpenzuar varet linearisht nga numri i përdoruesve. Por, pasi marrja e numrit të repo nuk varet një nga tjetri, proceset mund të paralelizohen. Për këtë, nevojitet të punoni me .map dhe 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 merr si input një array premisash me kthimin e një promise. Kjo e fundit përfundon pas përfundimit të të gjitha premisave në array ose në rastin e parë të refuzimit. Mund të ndodhë që ato të mos fillojnë të gjitha njëherësh; për të siguruar fillimin e njëkohshëm, mund të përdorni p-map.
Përfundim
Funksionet asinkrone po bëhen gjithnjë e më të rëndësishme për zhvillimin. Ndërsa për përdorimin adaptiv të funksioneve asinkrone, është e nevojshme të përdorni . Një zhvillues JavaScript duhet të ketë njohuri të mira në këtë fushë.
Skillbox rekomandon:
- Kurs praktik .
- Kurs online aplikativ .
- Kursi praktik dyvjeçar .
Burimi: habr.com
