Եկեք նայենք Async/Await-ին JavaScript-ում՝ օգտագործելով օրինակներ

Հոդվածի հեղինակը ուսումնասիրում է Async/Await-ի օրինակները JavaScript-ում: Ընդհանուր առմամբ, Async/Await-ը հարմար միջոց է ասինխրոն կոդ գրելու համար: Մինչ այս ֆունկցիայի հայտնվելը, նման ծածկագիրը գրվել է հետադարձ զանգերի և խոստումների միջոցով։ Բնօրինակ հոդվածի հեղինակը բացահայտում է Async/Await-ի առավելությունները՝ վերլուծելով տարբեր օրինակներ։

Հիշեցում. «Habr»-ի բոլոր ընթերցողների համար՝ 10 ռուբլի զեղչ «Habr» գովազդային կոդով Skillbox-ի ցանկացած դասընթացին գրանցվելիս:

Skillbox-ը խորհուրդ է տալիս. Ուսումնական առցանց դասընթաց «Java մշակող».

Հիշելու

Հետադարձ զանգը գործառույթ է, որի զանգը հետաձգվում է անորոշ ժամանակով: Նախկինում հետադարձ զանգերն օգտագործվում էին կոդի այն տարածքներում, որտեղ արդյունքը հնարավոր չէր անմիջապես ստանալ:

Ահա Node.js-ում ֆայլի ասինխրոն ընթերցման օրինակ.

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

Խնդիրներն առաջանում են, երբ անհրաժեշտ է միանգամից մի քանի ասինքրոն գործողություններ կատարել: Եկեք պատկերացնենք այս սցենարը՝ հարցում է արվում 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()-ը ասինխրոն ֆունկցիա է, որը կամ հաջողվում է («կատարյալ թիվ») կամ ձախողվում է սխալով («Ներողություն, թիվը չափազանց մեծ է»):

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Քանի որ վերը նշված օրինակը ակնկալում է, որ canRejectOrReturn-ը գործարկվի, դրա սեփական ձախողումը կհանգեցնի catch բլոկի կատարմանը: Արդյունքում, foo ֆունկցիան կավարտվի կա՛մ չսահմանված (երբ փորձարկման բլոկում ոչինչ չի վերադարձվում), կա՛մ հայտնաբերված սխալով: Արդյունքում, այս գործառույթը չի ձախողվի, քանի որ try/catch-ն ինքն է կառավարելու գործառույթը foo-ն:

Ահա ևս մեկ օրինակ.

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Արժե ուշադրություն դարձնել այն փաստին, որ օրինակում canRejectOrReturn-ը վերադարձվել է foo-ից։ Foo-ն այս դեպքում կամ ավարտվում է կատարյալ թվով, կամ վերադարձնում է Սխալ («Կներեք, թիվը չափազանց մեծ է»): Catch բլոկը երբեք չի գործարկվի:

Խնդիրն այն է, որ foo-ն վերադարձնում է canRejectOrReturn-ից տրված խոստումը: Այսպիսով, foo-ի լուծումը դառնում է canRejectOrReturn-ի լուծում: Այս դեպքում կոդը բաղկացած կլինի ընդամենը երկու տողից.

try {
    const promise = canRejectOrReturn();
    return promise;
}

Ահա թե ինչ է տեղի ունենում, եթե օգտագործեք սպասել և միասին վերադառնաք.

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Վերևի կոդի մեջ foo-ն հաջողությամբ դուրս կգա և՛ կատարյալ թվով, և՛ սխալմամբ: Այստեղ մերժումներ չեն լինի։ Բայց foo-ն կվերադառնա canRejectOrReturn-ով, ոչ թե undefined-ով: Եկեք համոզվենք դրանում՝ հեռացնելով վերադարձի սպասում canRejectOrReturn() տողը.

try {
    const value = await canRejectOrReturn();
    return value;
}
// …

Ընդհանուր սխալներ և որոգայթներ

Որոշ դեպքերում Async/Await-ի օգտագործումը կարող է հանգեցնել սխալների:

Մոռացված սպասում

Դա տեղի է ունենում բավականին հաճախ. սպասել հիմնաբառը մոռացվում է խոստումից առաջ.

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}

Ինչպես տեսնում եք, ծածկագրում սպասել կամ վերադարձ չկա: Հետևաբար foo-ն միշտ դուրս է գալիս չսահմանվածով առանց 1 վայրկյան ուշացման: Բայց խոստումը կկատարվի։ Եթե ​​այն սխալ կամ մերժում է առաջացնում, ապա UnhandledPromiseRejectionWarning-ը կկանչվի:

Async գործառույթները Callbacks-ում

Async ֆունկցիաները բավականին հաճախ օգտագործվում են .map կամ .filter-ում որպես հետադարձ զանգ: Օրինակ՝ fetchPublicReposCount(username) ֆունկցիան է, որը վերադարձնում է GitHub-ի բաց պահոցների քանակը։ Ենթադրենք, կան երեք օգտատերեր, որոնց չափումները մեզ անհրաժեշտ են: Ահա այս առաջադրանքի կոդը.

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'];
}

Մեզ պետք են ArfatSalman, octocat, norvig հաշիվներ։ Այս դեպքում մենք անում ենք.

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];
 
const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

Արժե ուշադրություն դարձնել 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-ի հետ:

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}

Promise.all-ը ստանում է մի շարք խոստումներ որպես մուտքագրում և վերադարձնում խոստում: Վերջինս, զանգվածի բոլոր խոստումների ավարտից հետո կամ առաջին մերժման դեպքում, ավարտվում է: Կարող է պատահել, որ դրանք բոլորը չսկսվեն միաժամանակ. միաժամանակյա մեկնարկն ապահովելու համար կարող եք օգտագործել p-map:

Ամփոփում

Async գործառույթները գնալով ավելի կարևոր են դառնում զարգացման համար: Դե, async գործառույթների հարմարվողական օգտագործման համար դուք պետք է օգտագործեք Async Iterators. JavaScript ծրագրավորողը պետք է լավ տիրապետի դրան:

Skillbox-ը խորհուրդ է տալիս.

Source: www.habr.com

Добавить комментарий