Եկեք նայենք Async/Await-ին JavaScript-ում՝ օգտագործելով օրինակներ
Հոդվածի հեղինակը ուսումնասիրում է Async/Await-ի օրինակները JavaScript-ում: Ընդհանուր առմամբ, Async/Await-ը հարմար միջոց է ասինխրոն կոդ գրելու համար: Մինչ այս ֆունկցիայի հայտնվելը, նման ծածկագիրը գրվել է հետադարձ զանգերի և խոստումների միջոցով։ Բնօրինակ հոդվածի հեղինակը բացահայտում է Async/Await-ի առավելությունները՝ վերլուծելով տարբեր օրինակներ։
Հիշեցում.«Habr»-ի բոլոր ընթերցողների համար՝ 10 ռուբլի զեղչ «Habr» գովազդային կոդով Skillbox-ի ցանկացած դասընթացին գրանցվելիս:
Skillbox-ը խորհուրդ է տալիս. Ուսումնական առցանց դասընթաց «Java մշակող».
Հիշելու
Հետադարձ զանգը գործառույթ է, որի զանգը հետաձգվում է անորոշ ժամանակով: Նախկինում հետադարձ զանգերն օգտագործվում էին կոդի այն տարածքներում, որտեղ արդյունքը հնարավոր չէր անմիջապես ստանալ:
Խնդիրներն առաջանում են, երբ անհրաժեշտ է միանգամից մի քանի ասինքրոն գործողություններ կատարել: Եկեք պատկերացնենք այս սցենարը՝ հարցում է արվում Arfat-ի օգտատերերի տվյալների բազային, պետք է կարդալ դրա profile_img_url դաշտը և նկար ներբեռնել someserver.com սերվերից։
Ներբեռնելուց հետո պատկերը փոխակերպում ենք այլ ձևաչափի, օրինակ՝ PNG-ից JPEG-ի։ Եթե փոխակերպումը հաջող է եղել, նամակ է ուղարկվում օգտատիրոջ էլ. Հաջորդը, իրադարձության մասին տեղեկատվությունը մուտքագրվում է transformations.log ֆայլում՝ նշելով ամսաթիվը:
Արժե ուշադրություն դարձնել կոդի վերջին մասում հետադարձ զանգերի համընկնմանը և })-ի մեծ քանակին։ Այն կոչվում է Callback Hell կամ Pyramid of Doom:
Այս մեթոդի թերությունները ակնհայտ են.
Այս կոդը դժվար է կարդալ:
Դժվար է նաև սխալների հետ վարվել, ինչը հաճախ հանգեցնում է կոդի վատ որակի:
Այս խնդիրը լուծելու համար JavaScript-ում խոստումներ են ավելացվել։ Նրանք թույլ են տալիս փոխարինել հետադարձ կապերի խորը բույնը .ապա բառով:
Խոստումների դրական կողմն այն է, որ դրանք ավելի լավ ընթեռնելի են դարձնում կոդը՝ վերևից ներքև, քան ձախից աջ: Սակայն խոստումներն ունեն նաև իրենց խնդիրները.
Դուք պետք է շատ .ապա ավելացնեք:
Փորձել/catch-ի փոխարեն .catch-ն օգտագործվում է բոլոր սխալները կարգավորելու համար:
Մի օղակում մի քանի խոստումների հետ աշխատելը միշտ չէ, որ հարմար է, որոշ դեպքերում դրանք բարդացնում են կոդը:
Ահա մի խնդիր, որը ցույց կտա վերջին կետի իմաստը.
Ենթադրենք, մենք ունենք for loop, որը տպում է 0-ից 10 թվերի հաջորդականություն պատահական ընդմիջումներով (0–n վայրկյան): Օգտագործելով խոստումները, դուք պետք է փոխեք այս օղակը, որպեսզի թվերը տպվեն հաջորդականությամբ 0-ից մինչև 10: Այսպիսով, եթե զրոյական տպագրությունը տևում է 6 վայրկյան, իսկ մեկը տպելու համար՝ 2 վայրկյան, ապա նախ պետք է տպվի զրոն, այնուհետև: կսկսվի տպագրության հետհաշվարկը:
Եվ իհարկե, այս խնդիրը լուծելու համար մենք չենք օգտագործում Async/Await կամ .sort: Օրինակի լուծումը վերջում է:
Async գործառույթներ
Async ֆունկցիաների ավելացումը ES2017-ում (ES8) պարզեցրել է խոստումների հետ աշխատելու խնդիրը: Ես նշում եմ, որ async գործառույթները աշխատում են խոստումների «վերևում»: Այս գործառույթները չեն ներկայացնում որակապես տարբեր հասկացություններ։ Async գործառույթները նախատեսված են որպես այլընտրանքային կոդի, որն օգտագործում է խոստումներ:
Async/Await-ը հնարավորություն է տալիս կազմակերպել աշխատանքը ասինխրոն կոդի հետ համաժամանակյա ոճով:
Այսպիսով, խոստումների իմացությունը հեշտացնում է Async/Await-ի սկզբունքները հասկանալը:
շարահյուսություն
Սովորաբար այն բաղկացած է երկու հիմնաբառից՝ async և սպասել: Առաջին բառը ֆունկցիան վերածում է ասինխրոնի։ Նման գործառույթները թույլ են տալիս օգտագործել սպասել: Ցանկացած այլ դեպքում, այս ֆունկցիայի օգտագործումը սխալ կառաջացնի:
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
Async-ը տեղադրվում է ֆունկցիայի հռչակագրի հենց սկզբում, իսկ սլաք ֆունկցիայի դեպքում՝ «=» նշանի և փակագծերի միջև:
Այս գործառույթները կարող են տեղադրվել օբյեկտում որպես մեթոդներ կամ օգտագործվել դասի հայտարարագրման մեջ:
// 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! Հարկ է հիշել, որ դասի կառուցողները և ստացողները/սահմանողները չեն կարող լինել ասինքրոն։
Իմաստաբանություն և կատարման կանոններ
Async գործառույթները հիմնականում նման են ստանդարտ JS գործառույթներին, սակայն կան բացառություններ:
Այսպիսով, async ֆունկցիաները միշտ խոստումներ են տալիս.
async function fn() {
return 'hello';
}
fn().then(console.log)
// hello
Մասնավորապես, fn-ը վերադարձնում է hello տողը: Դե, քանի որ սա ասինխրոն ֆունկցիա է, տողի արժեքը փաթաթված է խոստման մեջ՝ օգտագործելով կոնստրուկտոր:
Ահա այլընտրանքային դիզայն առանց Async-ի.
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// hello
Այս դեպքում խոստումը վերադարձվում է «ձեռքով»։ Ասինխրոն ֆունկցիան միշտ փաթաթված է նոր խոստումով:
Եթե վերադարձվող արժեքը պարզունակ է, ապա async ֆունկցիան վերադարձնում է արժեքը՝ այն փաթաթելով խոստման մեջ: Եթե վերադարձվող արժեքը խոստացված օբյեկտ է, ապա դրա լուծումը վերադարձվում է նոր խոստումով:
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Բայց ի՞նչ կլինի, եթե ասինխրոն ֆունկցիայի ներսում սխալ կա:
async function foo() {
throw Error('bar');
}
foo().catch(console.log);
Եթե այն չմշակվի, foo()-ը կվերադարձնի խոստումը մերժումով: Այս իրավիճակում Promise.reject-ը, որը պարունակում է սխալ, կվերադարձվի Promise.resolve-ի փոխարեն:
Async ֆունկցիաները միշտ խոստում են տալիս՝ անկախ նրանից, թե ինչ է վերադարձվում:
Ասինխրոն գործառույթները դադարեցվում են յուրաքանչյուր սպասման ժամանակ:
Սպասելն ազդում է արտահայտությունների վրա: Այսպիսով, եթե արտահայտությունը խոստում է, ապա async ֆունկցիան կասեցվում է մինչև խոստումը կատարվի։ Եթե արտահայտությունը խոստում չէ, այն փոխակերպվում է խոստման Promise.resolve-ի միջոցով, այնուհետև ավարտվում է:
// 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);
Եվ ահա նկարագրությունը, թե ինչպես է աշխատում fn ֆունկցիան։
Այն զանգահարելուց հետո առաջին տողը փոխարկվում է const a = սպասել 9-ից; in const a = սպասել Promise.resolve(9);.
Await-ն օգտագործելուց հետո ֆունկցիայի կատարումը կասեցվում է այնքան ժամանակ, մինչև a-ն ստանա իր արժեքը (ներկայիս իրավիճակում այն 9 է):
delayAndGetRandom(1000) դադարեցնում է fn ֆունկցիայի կատարումը մինչև այն ավարտվի ինքն իրեն (1 վայրկյան հետո): Սա արդյունավետորեն դադարեցնում է fn ֆունկցիան 1 վայրկյանով:
delayAndGetRandom(1000) միջոցով solution վերադարձնում է պատահական արժեք, որն այնուհետև վերագրվում է b փոփոխականին:
Դե, c փոփոխականի դեպքը նման է a փոփոխականի դեպքին: Դրանից հետո ամեն ինչ դադարում է մի վայրկյան, բայց այժմ delayAndGetRandom(1000) ոչինչ չի վերադարձնում, քանի որ դա պարտադիր չէ:
Արդյունքում, արժեքները հաշվարկվում են a + b * c բանաձևով: Արդյունքը փաթաթված է խոստումով, օգտագործելով Promise.resolve-ը և վերադարձվում է ֆունկցիայի կողմից:
Այս դադարները կարող են հիշեցնել ES6-ի գեներատորները, բայց դրանում ինչ-որ բան կա ձեր պատճառները.
Խնդրի լուծում
Դե, հիմա եկեք նայենք վերը նշված խնդրի լուծմանը։
finishMyTask ֆունկցիան օգտագործում է Await՝ սպասելու այնպիսի գործողությունների արդյունքներին, ինչպիսիք են queryDatabase, sendEmail, logTaskInFile և այլն: Եթե այս լուծումը համեմատեք այն լուծման հետ, որտեղ կիրառվել են խոստումներ, ապա նմանություններն ակնհայտ կդառնան։ Այնուամենայնիվ, Async/Await տարբերակը մեծապես պարզեցնում է բոլոր շարահյուսական բարդությունները: Այս դեպքում չկա մեծ թվով հետադարձ զանգեր և շղթաներ, ինչպիսիք են .then/.catch:
Ահա մի լուծում թվերի ելքով, կա երկու տարբերակ.
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);
});
});
};
Եվ ահա լուծում՝ օգտագործելով async ֆունկցիաները:
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}
Սխալ մշակելիս
Չմշակված սխալները փաթաթված են մերժված խոստման մեջ: Այնուամենայնիվ, async ֆունկցիաները կարող են օգտագործել try/catch՝ սխալները համաժամանակյա կարգավորելու համար:
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()-ը ասինխրոն ֆունկցիա է, որը կամ հաջողվում է («կատարյալ թիվ») կամ ձախողվում է սխալով («Ներողություն, թիվը չափազանց մեծ է»):
Քանի որ վերը նշված օրինակը ակնկալում է, որ canRejectOrReturn-ը գործարկվի, դրա սեփական ձախողումը կհանգեցնի catch բլոկի կատարմանը: Արդյունքում, foo ֆունկցիան կավարտվի կա՛մ չսահմանված (երբ փորձարկման բլոկում ոչինչ չի վերադարձվում), կա՛մ հայտնաբերված սխալով: Արդյունքում, այս գործառույթը չի ձախողվի, քանի որ try/catch-ն ինքն է կառավարելու գործառույթը foo-ն:
Արժե ուշադրություն դարձնել այն փաստին, որ օրինակում canRejectOrReturn-ը վերադարձվել է foo-ից։ Foo-ն այս դեպքում կամ ավարտվում է կատարյալ թվով, կամ վերադարձնում է Սխալ («Կներեք, թիվը չափազանց մեծ է»): Catch բլոկը երբեք չի գործարկվի:
Խնդիրն այն է, որ foo-ն վերադարձնում է canRejectOrReturn-ից տրված խոստումը: Այսպիսով, foo-ի լուծումը դառնում է canRejectOrReturn-ի լուծում: Այս դեպքում կոդը բաղկացած կլինի ընդամենը երկու տողից.
Վերևի կոդի մեջ foo-ն հաջողությամբ դուրս կգա և՛ կատարյալ թվով, և՛ սխալմամբ: Այստեղ մերժումներ չեն լինի։ Բայց foo-ն կվերադառնա canRejectOrReturn-ով, ոչ թե undefined-ով: Եկեք համոզվենք դրանում՝ հեռացնելով վերադարձի սպասում canRejectOrReturn() տողը.
Ինչպես տեսնում եք, ծածկագրում սպասել կամ վերադարձ չկա: Հետևաբար foo-ն միշտ դուրս է գալիս չսահմանվածով առանց 1 վայրկյան ուշացման: Բայց խոստումը կկատարվի։ Եթե այն սխալ կամ մերժում է առաջացնում, ապա UnhandledPromiseRejectionWarning-ը կկանչվի:
Async գործառույթները Callbacks-ում
Async ֆունկցիաները բավականին հաճախ օգտագործվում են .map կամ .filter-ում որպես հետադարձ զանգ: Օրինակ՝ fetchPublicReposCount(username) ֆունկցիան է, որը վերադարձնում է GitHub-ի բաց պահոցների քանակը։ Ենթադրենք, կան երեք օգտատերեր, որոնց չափումները մեզ անհրաժեշտ են: Ահա այս առաջադրանքի կոդը.
Արժե ուշադրություն դարձնել Await-ին .map callback-ում: Այստեղ հաշվարկները խոստումների մի շարք են, և .map-ը անանուն հետադարձ զանգ է յուրաքանչյուր նշված օգտվողի համար:
սպասման չափազանց հետևողական օգտագործումը
Եկեք այս կոդը վերցնենք որպես օրինակ.
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;
}
Այստեղ ռեպո համարը տեղադրվում է count փոփոխականում, ապա այս թիվը ավելացվում է counts զանգվածին։ Կոդի խնդիրն այն է, որ քանի դեռ առաջին օգտատիրոջ տվյալները չեն հասել սերվերից, բոլոր հաջորդ օգտվողները կլինեն սպասման ռեժիմում: Այսպիսով, միաժամանակ մշակվում է միայն մեկ օգտվող:
Եթե, օրինակ, մեկ օգտատիրոջ մշակման համար պահանջվում է մոտ 300 ms, ապա բոլոր օգտատերերի համար դա արդեն երկրորդն է՝ ծախսված ժամանակը գծայինորեն կախված է օգտատերերի քանակից։ Բայց քանի որ ռեպոների քանակի ձեռքբերումը կախված չէ միմյանցից, գործընթացները կարող են զուգահեռվել։ Սա պահանջում է աշխատել .map-ի և Promise.all-ի հետ:
Promise.all-ը ստանում է մի շարք խոստումներ որպես մուտքագրում և վերադարձնում խոստում: Վերջինս, զանգվածի բոլոր խոստումների ավարտից հետո կամ առաջին մերժման դեպքում, ավարտվում է: Կարող է պատահել, որ դրանք բոլորը չսկսվեն միաժամանակ. միաժամանակյա մեկնարկն ապահովելու համար կարող եք օգտագործել p-map:
Ամփոփում
Async գործառույթները գնալով ավելի կարևոր են դառնում զարգացման համար: Դե, async գործառույթների հարմարվողական օգտագործման համար դուք պետք է օգտագործեք Async Iterators. JavaScript ծրագրավորողը պետք է լավ տիրապետի դրան: