async/await-ի պրոֆեսիոնալ կիրառումը JavaScript-ում

async/await-ի պրոֆեսիոնալ կիրառումը JavaScript-ում

Յուրաքանչյուր ծրագրավորողի կյանքում գալիս է ժամանակ, երբ պետք է պարզել, թե ինչպես է աշխատում ասինքրոն կոդը:

Նրանց համար, ովքեր փորձում են առաջին անգամ հասկանալ, թե ինչպես եւ ինչ է կատարվում այստեղ, երբեմն դա կարող է մի քիչ վախեցնել: Բայց փորձենք հասկանալ: Առաջին հերթին պետք է հասկանալ սինքրոն եւ ասինքրոն կոդերի տարբերությունը։

Ի՞նչ է «ասինքրոն կոդ»- ը:

Ասինքրոնը տեղի է ունենում այն ժամանակ, երբ լինում են առնվազն երկու իրադարձություններ, որոնք տեղի են ունենում տարբեր ժամանակներում: Անդրադառնանք օրինակին.


console.log('hi')
console.log('my name')
console.log('is')
console.log('jeff!')

// Ելք: 
// hi
// my name
// is
// jeff!

Այստեղ կոդը կատարվում է ձեր ակնկալած հերթականությամբ: Նման կոդը կոչվում է սինքրոն կոդ։

Մեր գրածի մեծ մասը համարվում է սինքրոն։ Բայց փորձենք հետաձգել այս տողերից մեկի իրականացումը, որպեսզի մյուսները ավելի շուտ կատարվեն : Իսկ ի՞նչ է տեղի ունենում այդ ժամանակ ։

 
console.log('hi')
console.log('my name')
setTimeout(() => {
 console.log('is')
},500)
console.log('jeff!')
// Ելք:
// hi
// my name
// jeff!
// is

Ի՞նչ ստացանք ելքում. այժմ բառերի կարգը խառնված է, իսկ ցանկացած լեզվի համար կարեւոր է բառերի դասավորությունը: Ուստի առաջարկն այլեւս տրամաբանական չէ։ Ի դեպ, կոդը պետք է միաժամանակ աշխատի, սպասի, որ ժամկետը լրանա, եւ շարունակի իրականացումը։ Բայց ոչ այս դեպքում։

Իսկ վերը բերված օրինակում տեղի ունեցավ այն, որ ժամանակի ֆունկցիան՝timeout-ը կանչված էր կոդի մնացած մասի հետ միաժամանակ, սակայն ներքին log-ի գործարկումը սկսում է դրանից 500մվ հետո: Այստեղից էլ ելքում սխալ արդյունքի արտատպումը։

Կոդի ինչ-որ մասը կատարվում է հիմնական գործողությունների հերթից առանձին հերթականությամբ։ Սա կոչվում է ասինքրոնություն կամ ասինքրոն կանչ.

Իսկ ինչպե՞ս լուծել այս խնդիրը: Դա անելու համար անհրաժեշտ է մի միջոց, որը կկանգնեցնի ձեր կոդի մնացած մասը, մինչեւ որ ժամանակի ֆունկցիան`timeout-ը ավարտվի:

Բարեբախտաբար, JavaScript-ն ունի գործիքներ հենց դա անելու համար:

Promise

Պռոմիսները (Promise՝ անգլ․ – խոստում) նման են callback ֆունկցիաներին՝ նույնպես սպասում են ներքին կոդի ավարտին նախքան արժեքի վերադարձնելը։

Տարբերությունն այստեղ այն է, որ երբ ասինքրոն կոդը լրացված է, պռոմիսը վերադարձնում է մի վիճակ, որը թույլ է տալիս կատարել հետագա գործողությունները՝ օգտագործելով այնպիսի մեթոդներ, ինչպիսիք են then()-ը և catch()-ը։

Ուշադրություն․ այս հոդվածում մենք շատ մանրամասն չենք վերլուծի պռոմիսները, մեզ համար կարեւոր է ընդհանուր պատկերացում կազմել դրանց մասին։

Քննարկենք հետեւյալ օրինակը։ Ելքը նորից նորմալ է․


const async_func = () => {
 return new Promise((resolve,reject) => {
  setTimeout(() => {  
     resolve('is')
   },500)
 })
}

console.log('hi')
console.log('my name')

async_func().then(res => {
 console.log(res);
 console.log('jeff!');
}).catch(error => {
 console.error(error);
})

// Ելք:
// hi
// my name
// is
// jeff!

Բայց ինչպե՞ս դա տեղի ունեցավ։

Մինչեւ հիմա մենք տեսել ենք, որ պռոմիսները թույլ են տալիս սպասել, մինչեւ որ ասինքրոն կոդն ավարտվի, նախքան մնացած կոդը կկատարվի: Այս օրինակում մենք կրկին վերցրել ենք timeout ֆունկցիան և ներդրել պռոմիսի մեջ։

timeout ֆունկցիայի ավարտից հետո պռոմիսը վերադարձնում է resolve եւ կոդը տեղեկացվում է, որ այն կարող է շարունակել իրականացումը: Պռոմիսը հաջողությամբ կատարելուց հետո մնացած սինքրոն կոդը կիրառում է նույնը, ինչ նախկինում՝ then():

Իսկ ի՞նչ կլինի ելքի հետ, եթե resolve-ի փոխարեն պռոմիսը վերադարձնի reject։


const async_func = () => {
 return new Promise((resolve,reject) => {
  setTimeout(() => {  
     reject(false)
   },500)
 })
}

console.log('hi')
console.log('my name')

async_func().then(res => {
 console.log(res);
 console.log('jeff!');
}).catch(error => {
 console.error(error);
})

//Ելք:
// hi
// my name

Երբ պռոմիսը վերադարձնում է reject, կանչվում է catch() մեթոդը։ Մեր դեպքում այս մեթոդը մուտքագրում է սխալի հաղորդագրությունը եւ դադարեցնում է հաջորդող կոդի կատարումը: Երբ դա տեղի է ունենում, սխալը գրանցելու կարիք չկա:

Այսինքն՝ պայմանականորեն կարող ենք ասել, երբ պռոմիսը վերադարձնում է reject, մենք շարունակում ենք աշխատել կոդի մնացած մասով, ոչ թե կանգնում ենք սխալի վրա։

Պռոմիսների ներմուծումը մեծապես պարզեցնում է աշխատանքը ասինքրոն կոդով: Սակայն որքան մեծ է պռոմիսների շղթան, այնքան ավելի բարդ եւ դժվար ընկալելի է դարձնում կոդը։

Բայց կա արդյո՞ք ավելի լավ լուծում։

Async/await

JavaScript-ում օգտագործելով async/await բանալի բառը, մենք կոդը դարձնում ենք ավելի կոնկրետ և ավելի հարմար ընկալման համար։
Տեսնենք, թե ինչ է տեղի ունենում կոդի հետ, երբ ակտիվանում է async/await-ը:


const async_func = () => {
   return new Promise((resolve,reject) => {
      setTimeout(() => {
         resolve('is');
       },530)
     });
}

const main = async () => {
  console.log('hi')
  console.log('my name')
  const result = await async_func();
  console.log(result);
  console.log('jeff!')
}

main();

//Ելք:
// hi
// my name
// is
// jeff!

Հիմա կոդը ավելի լավ տեսք ունի։

Ելքը մնացել է նույնը, ինչ նախկինում։ Սակայն կոդն այդպես գրելը թույլ է տալիս դուրս գալ պռոմիսի շղթայից, այդքանով հանդերձ պահպանելով մարդկանց համար ընկալման հարմարավետությունը։

Նախորդ օրինակից վերցրել ենք նույն ասինքրոն կոդը, միայն այս անգամ նախանշել ենք await բանալի բառով և պահպանել պռոմիսից ստացած արդյունքը։

Կոդի օգտագործման ժամանակ main ֆունկցիան սպասում է թե երբ է async_func-ը վերադարձնելու resolve,իսկ հետո շարունակում է կոդի մնացած մասի իրականացումը։

Մենք ներդրել ենք նաև սինքրոն կոդի մնացած մասը main() ֆունկցիայի մեջ և օգտագործել ենք async բանալի բառը։ Ինչպես promise-ներն ունեն կանոններ, այնպես էլ մենք պետք է հետեւենք async/wait-ի կանոններին, որպեսզի դա աշխատի:

Այդ կանոններն ասում են՝ await բանալի բառը օգտագործելու համար անհրաժեշտ է դնել այն async ֆունկցիայի մեջ։ Սրանով ֆունկցիան կարծես տեղեկացնում է, որ իր ներսում սպասվում է ասինքրոնային կոդ:

Ենթադրենք՝ այժմ ուզում ենք մեկ ուրիշ ասինքրոնային կոդ ավելացնել։

Ի՞նչ է լինելու այս դեպքում:


const async_func = () => {
  return new Promise((resolve,reject) => {
   setTimeout(() => {
      resolve('is');
    },530)
  });
} 
  
 const async_func2 = () => {
 	return new Promise((resolve,reject) => {
   setTimeout(() => {
      resolve('and I love 2 code');
    },500)
  });
 } 

const main = async () => {
  console.log('hi')
  console.log('my name')
  const result = await async_func();
  console.log(result);
  console.log('jeff!')
  const result2 = await async_func2();
  console.log(result2)
}

main();

// Ելք:
// hi
// my name
// is
// jeff!
// and I love 2 code

Ուշադրություն դարձրեք, որ այստեղ ավելացնում ենք մեկ այլ ասինքրոն կոդի կտոր եւ ելքը մնում է նույն հերթականությամբ, ինչ որ սպասվում էր:

Տեսեք՝ երկրորդ ասինքրոն ֆունկցիան կանչվում է 30մվ ավելի շուտ, քան առաջինը: Այդ դեպքում ինչո՞ւ կաատարման հերթականությունը չպահպանվեց:

Սինքրոն իրավիճակում երկրորդ ֆունկցիան իսկապես կկանչվեր առաջինից առաջ եւ սխալ ելք տեղի կունենար:

Բայց երբ ակտիավանում է async/await-ը որը կանգ է առնում առաջին ֆունկցիայի ժամանակ եւ սպասում է 530մվ նախքան resolve-ի վերադառնալը։ Հետո հասնում է երկրորդ ֆունկցիային եւ 500մվ նույնպես սպասում մինչեւ resolve-ի վերադարձը։
Ահա թե ինչում է async/await-ի առավելություն։

Անդրադառնանք ավելի գործնական օրինակին՝ օգտագործելով async/await.


const getCharacters = () => {
    return new Promise((resolve,reject) => {
  	setTimeout(() => {
    	const characters = [
        'rick',
        'jerry',
        'morty',
        'rick',
        'kate',
        'summer',
        'summer',
        'kate',
        'noob noob',
        'jerry',
        'morty jr',
        'morty',
        'evil morty',
        'evil rick',
        'noob noob',
        'morty jr'
    ]
      resolve(characters);
    },300)
  })
}

const main = async () => {
    const characters = await getCharacters();
  const counts = characters.reduce((accumulator,character,index) => {
  	accumulator[character] ? accumulator[character].count++ : 
    accumulator[character] = {name: character, count: 1, index: index};
    return accumulator;
  },{})
  
  const imposters = Object.entries(counts)
  .filter(([key,obj]) => obj.count === 1)
  .map(([key,obj]) => ({name: key, index: obj.index}))
  
  console.log(imposters);
}

main();

//Ելք:
// [{name: 'evil morty', index: 12},{name: 'evil rick', index: 13}]

Այս օրինակում մենք իմիտացնում ենք backend-ին ուղղված հարցում եւ ստանում ենք այն մարդկանց ցանկը, որոնք անհրաժեշտ է ստուգել impostors-ի համար:

await-ը գործարկելուց հետո մենք սպասում ենք, որ main ֆունկցիան կվերադառնա resolve պռոմիսով, եւ ի պատասխան ստանում ենք պահանջված կերպարների ցանկը:

Այնուհետեւ մենք օգտագործում ենք reduce մեթոդը յուրաքանչյուր կերպարի համար եւ պահում ենք այն ինդեքսը, որով նրանք հայտնվել են, ինչպես նաեւ նրանց անունները:

Հետո counts օբյեկտը ֆիլտրում ենք ցանկացած կերպարի համար, որի հաշվիչը հավասար է մեկին, եւ այնուհետեւ համեմատում ենք imposters զանգվածի հետ:

Այնուհետեւ մենք ցուցադրում ենք imposter ցուցակը եւ ցույց ենք տալիս, թե որտեղ են նրանք գտնվում կերպարի տվյալների մեջ:
Եթե այս գործնական օրինակում մենք չօգտվեինք async/await-ից, ապա կոդում սխալներ կլինեին:

Ամեն ասինքրոն կոդ չէ, որ այդքան ակնհայտորեն զգուշացնում է խնդրի մասին, ուստի լավ գաղափար է սովորել հասկանալ, թե որտեղ են տեղի ունենում այս իրավիճակները և ինչպես լավագույնս վարվել դրանց հետ:

Ամփոփում

Ահա եւ վերջ:

Երբեմն դժվար է հասկանալ, թե ինչպես կարելի է աշխատել ասինքրոնային կոդի հետ, հատկապես, երբ առաջին անգամ ես գործ ունենում դրա հետ։ Որքան շատ եք հանդիպում ասինքրոնային կոդի, այնքան ավելի լավ եք հաղթահարում նման իրավիճակները:

Հուսով եմ, որ դուք հասկացաք, թե ինչպես է աշխատում async/await ծրագիրը եւ շատ շուտով կսկսեք ավելի լավ տիրապետել JavaScript-ին:

Սկզբնաղբյուր՝ How to Use Async/Await in JavaScript Like a Pro

Թարգմանությունը՝ Mariam Abrahamyan