ApskatÄ«sim JavaScript/Async/Await, izmantojot piemÄrus
Raksta autors aplÅ«ko JavaScript/Async/Await piemÄrus. KopumÄ Async/Await ir Ärts veids, kÄ rakstÄ«t asinhrono kodu. Pirms Ŕīs funkcijas parÄdÄ«Å”anÄs Å”Äds kods tika rakstÄ«ts, izmantojot atzvanus un solÄ«jumus. SÄkotnÄjÄ raksta autors, analizÄjot dažÄdus piemÄrus, atklÄj Async/Await priekÅ”rocÄ«bas.
AtzvanÄ«Å”ana ir funkcija, kuras zvans tiek aizkavÄts uz nenoteiktu laiku. IepriekÅ” atzvanÄ«Å”ana tika izmantota tajos koda apgabalos, kur rezultÄtu nevarÄja iegÅ«t uzreiz.
Å eit ir piemÄrs faila asinhronai lasÄ«Å”anai pakalpojumÄ Node.js:
ProblÄmas rodas, ja vienlaikus ir jÄveic vairÄkas asinhronas darbÄ«bas. IedomÄsimies Å”Ädu scenÄriju: tiek veikts pieprasÄ«jums Arfat lietotÄju datu bÄzei, jÄizlasa tÄs profils_img_url lauks un jÄlejupielÄdÄ attÄls no servera someserver.com.
PÄc lejupielÄdes mÄs pÄrvÄrÅ”am attÄlu citÄ formÄtÄ, piemÄram, no PNG uz JPEG. Ja konvertÄÅ”ana bija veiksmÄ«ga, uz lietotÄja e-pastu tiek nosÅ«tÄ«ta vÄstule. TÄlÄk informÄcija par notikumu tiek ievadÄ«ta transformations.log failÄ, norÄdot datumu.
Ir vÄrts pievÄrst uzmanÄ«bu atzvanÄ«Å”anas pÄrklÄÅ”anÄs un lielajam }) skaitam koda pÄdÄjÄ daļÄ. To sauc par AtzvanÄ«Å”anas elli vai NolemtÄ«bas piramÄ«du.
Šīs metodes trūkumi ir acīmredzami:
Šo kodu ir grūti nolasīt.
Ir arÄ« grÅ«ti rÄ«koties ar kļūdÄm, kas bieži noved pie sliktas koda kvalitÄtes.
Lai atrisinÄtu Å”o problÄmu, JavaScript tika pievienoti solÄ«jumi. Tie ļauj aizstÄt atzvanÄ«Å”anas dziļo ligzdoÅ”anu ar vÄrdu .then.
SolÄ«jumu pozitÄ«vais aspekts ir tas, ka tie padara kodu daudz labÄk lasÄmu, no augÅ”as uz leju, nevis no kreisÄs uz labo pusi. TomÄr solÄ«jumiem ir arÄ« savas problÄmas:
Jums jÄpievieno daudz .tad.
Try/catch vietÄ, lai apstrÄdÄtu visas kļūdas, tiek izmantots .catch.
Darbs ar vairÄkiem solÄ«jumiem vienÄ cilpÄ ne vienmÄr ir Ärti; dažos gadÄ«jumos tie sarežģī kodu.
Å eit ir problÄma, kas parÄdÄ«s pÄdÄjÄ punkta nozÄ«mi.
PieÅemsim, ka mums ir for cilpa, kas izdrukÄ skaitļu virkni no 0 lÄ«dz 10 ar nejauÅ”iem intervÄliem (0ān sekundes). Izmantojot solÄ«jumus, Ŕī cilpa ir jÄmaina, lai skaitļi tiktu drukÄti secÄ«gi no 0 lÄ«dz 10. TÄtad, ja nulles izdrukÄÅ”anai nepiecieÅ”amas 6 sekundes un vieninieka izdrukÄÅ”anai nepiecieÅ”amas 2 sekundes, vispirms jÄdrukÄ nulle un pÄc tam sÄksies atpakaļskaitÄ«Å”ana drukÄÅ”anai.
Un, protams, mÄs neizmantojam Async/Await vai .sort, lai atrisinÄtu Å”o problÄmu. RisinÄjuma piemÄrs ir beigÄs.
AsinhronÄs funkcijas
Asinhrono funkciju pievienoÅ”ana ES2017 (ES8) vienkÄrÅ”oja uzdevumu strÄdÄt ar solÄ«jumiem. Es atzÄ«mÄju, ka asinhronÄs funkcijas darbojas āpapildusā solÄ«jumiem. Å Ä«s funkcijas neatspoguļo kvalitatÄ«vi atŔķirÄ«gus jÄdzienus. AsinhronÄs funkcijas ir paredzÄtas kÄ alternatÄ«va kodam, kas izmanto solÄ«jumus.
Async/Await ļauj organizÄt darbu ar asinhrono kodu sinhronÄ stilÄ.
TÄdÄjÄdi, zinot solÄ«jumus, ir vieglÄk saprast Async/Await principus.
sintakse
Parasti tas sastÄv no diviem atslÄgvÄrdiem: async un await. Pirmais vÄrds pÄrvÄrÅ” funkciju par asinhronu. Å Ädas funkcijas ļauj izmantot gaidÄ«Å”anu. JebkurÄ citÄ gadÄ«jumÄ Å”Ä«s funkcijas izmantoÅ”ana radÄ«s kļūdu.
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
Async tiek ievietots funkcijas deklarÄcijas paÅ”Ä sÄkumÄ un bultiÅas funkcijas gadÄ«jumÄ starp zÄ«mi ā=ā un iekavÄm.
Å Ä«s funkcijas var ievietot objektÄ kÄ metodes vai izmantot klases deklarÄcijÄ.
// 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! Ir vÄrts atcerÄties, ka klases konstruktori un ieguvÄji/iestatÄ«tÄji nevar bÅ«t asinhroni.
Semantika un izpildes noteikumi
AsinhronÄs funkcijas bÅ«tÄ«bÄ ir lÄ«dzÄ«gas standarta JS funkcijÄm, taÄu ir izÅÄmumi.
TÄdÄjÄdi asinhronÄs funkcijas vienmÄr atgriež solÄ«jumus:
async function fn() {
return 'hello';
}
fn().then(console.log)
// hello
KonkrÄti, fn atgriež virkni hello. TÄ kÄ Å”Ä« ir asinhrona funkcija, virknes vÄrtÄ«ba tiek iesaiÅota solÄ«jumÄ, izmantojot konstruktoru.
Šeit ir alternatīvs dizains bez async:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// hello
Å ajÄ gadÄ«jumÄ solÄ«jums tiek atgriezts āmanuÄliā. AsinhronÄ funkcija vienmÄr ir ietÄ«ta ar jaunu solÄ«jumu.
Ja atgrieÅ”anas vÄrtÄ«ba ir primitÄ«va, asinhronÄ funkcija atgriež vÄrtÄ«bu, iesaiÅojot to solÄ«jumÄ. Ja atgriežamÄ vÄrtÄ«ba ir solÄ«juma objekts, tÄ izŔķirtspÄja tiek atgriezta jaunÄ solÄ«jumÄ.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Bet kas notiek, ja asinhronajÄ funkcijÄ ir kļūda?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);
Ja tas netiek apstrÄdÄts, foo() atgriezÄ«s solÄ«jumu ar noraidÄ«jumu. Å ÄdÄ situÄcijÄ Promise.resolve vietÄ tiks atgriezts Promise.reject, kurÄ ir kļūda.
AsinhronÄs funkcijas vienmÄr izvada solÄ«jumu neatkarÄ«gi no tÄ, kas tiek atgriezts.
AsinhronÄs funkcijas tiek apturÄtas katrÄ gaidÄ«Å”anas reizÄ.
GaidÄ«Å”ana ietekmÄ izteiksmes. TÄtad, ja izteiksme ir solÄ«jums, asinhronÄ funkcija tiek apturÄta, lÄ«dz tiek izpildÄ«ts solÄ«jums. Ja izteiksme nav solÄ«jums, tÄ tiek pÄrveidota par solÄ«jumu, izmantojot Promise.resolve, un pÄc tam pabeigta.
// 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);
Un Å”eit ir aprakstÄ«ts, kÄ darbojas fn funkcija.
PÄc tÄ izsaukÅ”anas pirmÄ rinda tiek pÄrveidota no const a = await 9; in const a = gaidiet Promise.resolve(9);.
PÄc Await izmantoÅ”anas funkcijas izpilde tiek apturÄta, lÄ«dz a iegÅ«st savu vÄrtÄ«bu (paÅ”reizÄjÄ situÄcijÄ tÄ ir 9).
delayAndGetRandom(1000) aptur fn funkcijas izpildi, lÄ«dz tÄ tiek pabeigta pati (pÄc 1 sekundes). Tas efektÄ«vi aptur fn funkciju uz 1 sekundi.
delayAndGetRandom(1000), izmantojot risinÄjumu, atgriež nejauÅ”u vÄrtÄ«bu, kas pÄc tam tiek pieŔķirta mainÄ«gajam b.
GadÄ«jums ar mainÄ«go c ir lÄ«dzÄ«gs gadÄ«jumam ar mainÄ«go a. PÄc tam viss uz sekundi apstÄjas, bet tagad delayAndGetRandom(1000) neko neatgriež, jo tas nav nepiecieÅ”ams.
RezultÄtÄ vÄrtÄ«bas tiek aprÄÄ·inÄtas, izmantojot formulu a + b * c. RezultÄts tiek iesaiÅots solÄ«jumÄ, izmantojot Promise.resolve, un tiek atgriezts ar funkciju.
Å Ä«s pauzes var atgÄdinÄt ES6 Ä£eneratorus, taÄu tajÄ ir kaut kas jÅ«su iemesli.
ProblÄmas risinÄÅ”ana
Nu, tagad apskatÄ«sim iepriekÅ” minÄtÄs problÄmas risinÄjumu.
Funkcija finishMyTask izmanto Await, lai gaidÄ«tu tÄdu darbÄ«bu rezultÄtus kÄ queryDatabase, sendEmail, logTaskInFile un citas. Ja salÄ«dzinÄsiet Å”o risinÄjumu ar to, kurÄ tika izmantoti solÄ«jumi, lÄ«dzÄ«bas kļūs acÄ«mredzamas. TomÄr Async/Await versija ievÄrojami vienkÄrÅ”o visas sintaktiskÄs sarežģītÄ«bas. Å ajÄ gadÄ«jumÄ nav daudz atzvanÄ«Å”anas gadÄ«jumu un Ä·Äžu, piemÄram, .then/.catch.
Å eit ir risinÄjums ar skaitļu izvadi, ir divas iespÄjas.
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);
});
});
};
Un Å”eit ir risinÄjums, izmantojot asinhronÄs funkcijas.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}
Kļūda apstrÄdÄ
NeapstrÄdÄtas kļūdas ir iesaiÅotas noraidÄ«tÄ solÄ«jumÄ. TomÄr asinhronÄs funkcijas var izmantot try/catch, lai sinhroni apstrÄdÄtu kļūdas.
async function canRejectOrReturn() {
// wait one second
await new Promise(res => setTimeout(res, 1000));
// Reject with ~50% probability
if (Math.random() > 0.5) {
throw new Error('Sorry, number too big.')
}
return 'perfect number';
}
canRejectOrReturn() ir asinhrona funkcija, kas ir veiksmÄ«ga (āideÄls skaitlisā) vai neizdodas ar kļūdu (āAtvainojiet, skaitlis ir pÄrÄk lielsā).
TÄ kÄ iepriekÅ” minÄtajÄ piemÄrÄ ir paredzÄts izpildÄ«t canRejectOrReturn, tÄ paÅ”a kļūme izraisÄ«s uztverÅ”anas bloka izpildi. RezultÄtÄ funkcija foo beigsies ar nedefinÄtu (kad nekas netiek atgriezts mÄÄ£inÄjuma blokÄ), vai arÄ« ar kļūdu. RezultÄtÄ Å”Ä« funkcija neizdosies, jo try/catch apstrÄdÄs paÅ”u funkciju foo.
Ir vÄrts pievÄrst uzmanÄ«bu faktam, ka piemÄrÄ canRejectOrReturn tiek atgriezts no foo. Foo Å”ajÄ gadÄ«jumÄ vai nu beidzas ar perfektu skaitli, vai atgriež kļūdu (āAtvainojiet, skaitlis ir pÄrÄk lielsā). NoÄ·erÅ”anas bloks nekad netiks izpildÄ«ts.
ProblÄma ir tÄda, ka foo atgriež solÄ«jumu, kas tika dots no canRejectOrReturn. TÄtad risinÄjums foo kļūst par risinÄjumu canRejectOrReturn. Å ajÄ gadÄ«jumÄ kods sastÄvÄs tikai no divÄm rindÄm:
IepriekÅ” minÄtajÄ kodÄ foo veiksmÄ«gi izies gan ar perfektu numuru, gan pieÄ·erot kļūdu. Å eit nebÅ«s nekÄdu atteikumu. Bet foo atgriezÄ«sies ar canRejectOrReturn, nevis ar undefined. PÄrliecinÄsimies par to, noÅemot rindu return await canRejectOrReturn():
KÄ redzat, kodÄ nav gaidÄ«Å”anas vai atgrieÅ”anas. TÄpÄc foo vienmÄr iziet ar undefined bez 1 sekundes aizkaves. Bet solÄ«jums tiks izpildÄ«ts. Ja tas rada kļūdu vai noraidÄ«Å”anu, tiks izsaukts UnhandledPromiseRejectionWarning.
AsinhronÄs funkcijas atzvanÄ«jumos
AsinhronÄs funkcijas diezgan bieži tiek izmantotas .map vai .filter kÄ atzvanÄ«Å”anas iespÄjas. PiemÄrs ir funkcija fetchPublicReposCount(lietotÄjvÄrds), kas atgriež GitHub atvÄrto repozitoriju skaitu. PieÅemsim, ka ir trÄ«s lietotÄji, kuru rÄdÄ«tÄji mums ir nepiecieÅ”ami. Å eit ir Ŕī uzdevuma kods:
Ir vÄrts pievÄrst uzmanÄ«bu Await .map atzvanÄ«Å”anai. Å eit skaits ir solÄ«jumu klÄsts, un .map ir anonÄ«ma atzvanÄ«Å”ana katram norÄdÄ«tajam lietotÄjam.
PÄrÄk konsekventa gaidÄ«Å”anas izmantoÅ”ana
Å emsim Å”o kodu kÄ piemÄru:
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;
}
Å eit repo numurs tiek ievietots skaitÄ«Å”anas mainÄ«gajÄ, pÄc tam Å”is skaitlis tiek pievienots skaitÄ«Å”anas masÄ«vam. ProblÄma ar kodu ir tÄda, ka lÄ«dz brÄ«dim, kad no servera pienÄk pirmÄ lietotÄja dati, visi nÄkamie lietotÄji bÅ«s gaidÄ«Å”anas režīmÄ. TÄdÄjÄdi vienlaikus tiek apstrÄdÄts tikai viens lietotÄjs.
Ja, piemÄram, viena lietotÄja apstrÄde aizÅem apmÄram 300 ms, tad visiem lietotÄjiem tÄ ir jau sekunde, patÄrÄtais laiks lineÄri ir atkarÄ«gs no lietotÄju skaita. Bet, tÄ kÄ repo skaita iegÅ«Å”ana nav viena no otras atkarÄ«ga, procesus var paralÄli. Tam nepiecieÅ”ams strÄdÄt ar .map un Promise.all:
Promise.all kÄ ievadi saÅem daudzus solÄ«jumus un atgriež solÄ«jumu. PÄdÄjais tiek izpildÄ«ts pÄc tam, kad visi masÄ«vÄ solÄ«jumi ir izpildÄ«ti vai pÄc pirmÄ noraidÄ«Å”anas. Var gadÄ«ties, ka tie visi nestartÄjas vienlaicÄ«gi ā lai nodroÅ”inÄtu vienlaicÄ«gu startu, var izmantot p-map.
SecinÄjums
AsinhronÄs funkcijas kļūst arvien svarÄ«gÄkas attÄ«stÄ«bai. Lai adaptÄ«vi izmantotu asinhronÄs funkcijas, jums vajadzÄtu izmantot Asinhronie iteratori. JavaScript izstrÄdÄtÄjam tas ir labi jÄpÄrzina.