Autori i artikullit shpjegon me shembuj Async/Await në JavaScript. Në thelb, Async/Await është një mënyrë e këndshme për të shkruar kod asinkron. Përdorimi i kësaj mundësie i ka dhënë fund ndihmës duke përdorur thirrje dhe premisa. Autori i artikullit origjinal zbulon përfitimet e Async/Await duke analizuar shembuj të ndryshëm.
Kujtojmë: për të gjithë lexuesit e "Habra" — zbritje prej 10,000 rublej për regjistrimin në çdo kurs Skillbox me kodin promocional "Habr".
Skillbox rekomandon: Kurs online edukativ .
Callback
Callback-i është një funksion, thirrja e të cilit vonohet për një periudhë të pacaktuar. Më parë, thirrjet e prapambetura përdoren në ato pjesë të kodit ku rezultati nuk mund të merret 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 ndodhin atëherë kur është e nevojshme të kryhen disa operacione asinkrone në të njëjtën kohë. 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 pastrimit, ne konvertojmë imazhin në një format tjetër, për shembull nga PNG në JPEG. Nëse konvertimi është kryer me sukses, një email dërgohet te përdoruesi. Më pas, informacioni mbi ngjarjen regjistrohet në skedarin transformations.log me datën e saktë.
Është e rëndësishme të kuptohet ndërlikimi i thirrjeve të prapshme dhe numri i madh i }) në pjesën përfundimtare të kodit. Kjo quhet Hell i Thirrjeve të Prapshme ose Piramida e Dëmtimit.
Disavantazhet e këtij mënyre janë të dukshme:
- Ky kod është i vështirë për t'u lexuar.
- Është gjithashtu e vështirë të trajtohen gabimet, që shpesh çon në një përkeqësim të cilësisë së kodit.
Për të zgjidhur këtë problem, në JavaScript u shtuan premisat. Ato lejojnë zëvendësimin e thellësisë së thirrjeve të prapshme me fjalën .then.
Një aspekt pozitiv i premisave është se me to kodi lexohet shumë më mirë, nga lartë poshtë, dhe jo nga majtas djathtas. Megjithatë, ka edhe disa probleme me premisat:
- Duhet të shtohen shumë .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 komplikojnë kodin.
Ja një detyrë që do të tregojë vlerën e pikës së fundit.
Supozoni se ka ekziston 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ë një mënyrë që numrat të printohen në renditje nga 0 në 10. Kështu, nëse printimi i zeros zgjat 6 sekonda dhe i njëshit 2 sekonda, në fillim duhet të printohet zeroja, dhe pastaj të fillojë numërimi i printimit të njëshit.
Sigurisht, për të zgjidhur këtë problem, ne nuk përdorim Async/Await ose .sort. Një shembull zgjidhjeje do të jetë në fund.
Funksionet Async
Shtimi i funksioneve async në ES2017 (ES8) e ka thjeshtuar punën me premtimet. Vlen të theksohet se funksionet async punojnë "përmbi" premtimet. Këto funksione nuk paraqesin koncepte cilësisht të tjera. 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ë një stil sinqchron.
Prandaj, njohja e premtimeve lehtëson kuptimin e principeve të Async/Await.
Sintaksa
Në situata të zakonshme, 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ëtij 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, ndërsa në rastin e përdorimit të funksionit të shkurtër — midis shenjës "=" dhe parantezave.
Këto funksione mund të vendosen në një objekt si metoda ose mund të përdoren në shpalljen e klasës.
// 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 konstruktorët e klasës dhe geterët/seterët nuk mund të jenë asinkronë.
Semantika dhe rregullat e ekzekutimit
Krijuesit async janë, në parim, të ngjashëm me funksionet standarde të JS, por ka përjashtime.
Kështu, funksionet async gjithmonë kthejnë premtime:
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloKonkretisht, fn kthen vargun hello. Dhe duke qenë se është një funksion asinkron, vlera e vargut mbështillet në një premtim me ndihmën e konstruktorit.
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 duar". 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ë premtim. Në rastin kur vlera e kthyer është vetë një objekt premtimi, zgjidhja e tij kthehet në një premtim të ri.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Por çfarë do të ndodhë nëse brenda funksionit async ndodhet një gabim?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);Nëse ai nuk trajtohet, foo() do të kthejë një premtim me refuzim. Në këtë situatë, në vend të Promise.resolve do të kthehet Promise.reject, që përmban gabimin.
Funksionet Async gjithmonë japin një premtim në dalje, pavarësisht nga ajo që kthehet.
Funksionet asinkrone pezullohen me çdo await.
Await ndikon në shprehjet. Pra, nëse shprehja është një premtim, funksioni async pezullohet deri në përfundimin e premtimit. Nëse shprehja nuk është një premtim, ajo konvertohet në premtim përmes Promise.resolve dhe pastaj 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.
- Pas thirrjes së tij, rreshti i parë konvertohet nga const a = await 9; në const a = await Promise.resolve(9);.
- Pas përdorimit të Await, ekzekutimi i funksionit pezullohet derisa të marrë vlerën e tij (në këtë rast kjo është 9).
- delayAndGetRandom(1000) pezullon ekzekutimin e funksionit fn derisa ai të përfundojë vetë (pas 1 sekonde). Kjo në thelb është një ndalesë e funksionit fn për 1 sekondë.
- delayAndGetRandom(1000) kthen një vlerë rastësore përmes resolve, e cila më pas i jepet variable b.
- Dhe rasti me variablën c është i njëjtë me atë me variablën a. Pas kësaj gjithçka ndalet për një sekondë, por tani delayAndGetRandom(1000) nuk kthen asgjë, pasi nuk është e nevojshme.
- Në fund, vlerat llogariten sipas formulës a + b * c. Rezultati mbyllet në një promise me ndihmën e Promise.resolve dhe kthehet nga funksioni.
Këto pauza mund të duken si gjeneratorët në ES6, por ka .
Zgjidhim problemin
Tani le të shqyrtojmë zgjidhjen e problemit që u përmend më lart.
Në funksionin finishMyTask, përdoret Await për të pritur rezultatet e operacioneve si queryDatabase, sendEmail, logTaskInFile dhe të tjera. Nëse e krahasojmë këtë zgjidhje me atë ku janë përdorur premtimet, ngjashmëria bëhet e dukshme. Megjithatë, versioni me Async/Await e thjeshton ndjeshëm të gjitha kompleksitetet sintaksore. Në këtë rast, nuk ka shumë kolbekë dhe vargje si .then/.catch.
Këtu është 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 me përdorimin e funksioneve 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 pa trajtuara mbështjellen në një premtim të refuzuar. Megjithatë, në funksionet async mund të përdoret konstrukti try/catch për të realizuar trajtimin sinqer të gabimeve.
async function canRejectOrReturn() {
// priti një sekondë
await new Promise(res => setTimeout(res, 1000));
// Refuzo me ~50% mundësi
if (Math.random() > 0.5) {
throw new Error('Kërkoj ndjesë, numri është shumë i madh.')
}
return 'numri perfekt';
}canRejectOrReturn() është një funksion asinkron, i cili ose përfundon me sukses (“numri perfekt”), ose dështon me një gabim (“Kërkoj ndjesë, numri është shumë i madh”).
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'gabimi u kap';
}
}Në shembullin e mësipërm, pasi pritet që canRejectOrReturn të ekzekutohet, një përfundim i dështuar i vetë funksionit do të shkaktojë ekzekutimin e bllokut catch. Si rezultat, funksioni foo do të përfundojë ose me undefined (kur në bllokun try nuk kthehet asgjë), ose me error caught. Si përfundim, ky funksion nuk do të ketë një përfundim të dështuar, pasi try/catch do të merret me përpunimin e funksionit foo.
Ja dhe një shembull tjetër:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}Është e rëndësishme të theksohet se në shembullin nga foo kthehet canRejectOrReturn. Foo në këtë rast përfundon ose me numër perfekt, ose kthen një gabim Error ("Më vjen keq, numri është shumë i madh"). Blloku catch kurrë nuk do të ekzekutohet.
Problemi është se foo kthen një promesë, e cila është kaluar 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ë ndodhë nëse përdoren së bashku await dhe return:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}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ë refuzime këtu. Por foo do të përfundojë me canRejectOrReturn, 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 harruar await
Kjo ndodh mjaft shpesh — para një premtimi harrohet fjala kyçe await:
funksioni async foo() {
provoni {
canRejectOrReturn();
} kap (e) {
return 'kapur';
}
}Në kod, siç shihet, nuk ka as await as return. Prandaj foo gjithmonë përfundon me undefined pa vonesë prej 1 sekonde. Por premisa do të ekzekutohet. Nëse ajo jep një gabim ose refuzon, atëherë do të thirret UnhandledPromiseRejectionWarning.
Async-funksionet në thirrjet e kthimit
Async-funksionet përdoren mjaft shpesh në .map ose .filter si thirrje të kthimit. Një shembull mund të jetë funksioni fetchPublicReposCount(username), i cili kthen numrin e depozitave të hapura në GitHub. Supozoni se ka tre përdoruesish, të cilët kanë treguesit që na nevojiten. Ja kodi për këtë detyrë:
const url = 'https://api.github.com/users';
// Funksioni utilitar për të marrë numrat e repove
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}Na nevojiten llogaritë ArfatSalman, octocat, norvig. Në këtë rast, realizojmë:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});Vlen të përmendet Await në thirrjen e prapme .map. Këtu counts është një masiv premtimesh, ndërsa .map është një thirrje e panjohur për çdo përdorues të caktuar.
Përdorimi i tepruar sekuencial i await
Si shembull, le të marrim këtë 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;
}Këtu në variablën count vendoset numri i repo-ve, pastaj ky numër shtohet në masivin counts. Problemi i kodit është se derisa të vijnë të dhënat e përdoruesit të parë nga serveri, të gjithë përdoruesit e tjerë do të jenë në pritje. Pra, në një moment të vetëm, përvelled vetëm një përdorues.
Për shembull, nëse duhen rreth 300 ms për të përpunuar një përdorues, atëherë për të gjithë përdoruesit kjo është një sekondë; koha e kaluar varet linear nga numri i përdoruesve. Por pasi marrja e numrit të repove nuk varet një nga tjetri, proceset mund të paralelizohen. Për këtë nevojitet puna 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 premtimesh që kthejnë një premtim. Ai përfundon pasi përfundojnë të gjitha premtimet në array ose në rastin e parë të refuzimit. Mund të ndodhë që të gjitha ato mos të nisin në të njëjtën kohë; për të siguruar fillimin e njëkohshëm, mund të përdorim p-map.
Përfundimi
Funksionet Async bëhen gjithnjë e më të rëndësishme për zhvillimin. Dhe për përdorim adaptiv të funksioneve async, është mirë të përdorim . Një zhvillues JavaScript duhet të kuptojë mirë këtë.
Skillbox rekomandon:
- Kurs praktik .
- Kursi online i aplikimit .
- Kurs praktik dyvjeçar .
Burimi: habr.com
