Keling, misollar yordamida JavaScript-da Async/Await-ni ko'rib chiqaylik

Maqola muallifi JavaScript-da Async/Await misollarini ko'rib chiqadi. Umuman olganda, Async/Await asinxron kod yozishning qulay usuli hisoblanadi. Ushbu xususiyat paydo bo'lishidan oldin, bunday kod qayta qo'ng'iroqlar va va'dalar yordamida yozilgan. Asl maqola muallifi turli misollarni tahlil qilish orqali Async/Await ning afzalliklarini ochib beradi.

Sizga eslatib o'tamiz: "Habr" ning barcha o'quvchilari uchun - "Habr" promo-kodidan foydalangan holda har qanday Skillbox kursiga yozilishda 10 000 rubl chegirma.

Skillbox tavsiya qiladi: Ta'lim onlayn kurs "Java dasturchisi".

Callback

Qayta qo'ng'iroq - bu qo'ng'iroq noma'lum muddatga kechiktiriladigan funksiya. Ilgari qayta qo'ng'iroqlar natijani darhol olish mumkin bo'lmagan kod sohalarida ishlatilgan.

Node.js da faylni asinxron o‘qishga misol:

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

Bir vaqtning o'zida bir nechta asenkron operatsiyalarni bajarish kerak bo'lganda muammolar paydo bo'ladi. Keling, ushbu stsenariyni tasavvur qilaylik: Arfat foydalanuvchi ma'lumotlar bazasiga so'rov yuboriladi, siz uning profile_img_url maydonini o'qib chiqishingiz va someserver.com serveridan rasmni yuklab olishingiz kerak.
Yuklab olgandan so'ng, biz tasvirni boshqa formatga, masalan, PNG-dan JPEG-ga o'zgartiramiz. Agar konvertatsiya muvaffaqiyatli bo'lsa, foydalanuvchining elektron pochtasiga xat yuboriladi. Keyinchalik, sanani ko'rsatgan holda, voqea haqidagi ma'lumotlar transformations.log fayliga kiritiladi.

Kodning yakuniy qismida qayta qo'ng'iroqlarning bir-biriga mos kelishiga va ko'p sonli }) ga e'tibor qaratish lozim. U "Callback Hell" yoki "Doom piramidasi" deb ataladi.

Ushbu usulning kamchiliklari aniq:

  • Ushbu kodni o'qish qiyin.
  • Xatolarni hal qilish ham qiyin, bu ko'pincha yomon kod sifatiga olib keladi.

Ushbu muammoni hal qilish uchun JavaScript-ga va'dalar qo'shildi. Ular qayta qo'ng'iroqlarni chuqur joylashtirishni .then so'zi bilan almashtirishga imkon beradi.

Va'dalarning ijobiy tomoni shundaki, ular kodni chapdan o'ngga emas, yuqoridan pastga qarab ancha yaxshi o'qilishi mumkin. Biroq, va'dalar ham o'z muammolariga ega:

  • Siz ko'p .keyin qo'shishingiz kerak.
  • Barcha xatolarni qayta ishlash uchun try/catch o'rniga .catch ishlatiladi.
  • Bitta tsiklda bir nechta va'dalar bilan ishlash har doim ham qulay emas, ba'zi hollarda ular kodni murakkablashtiradi.

Mana, oxirgi nuqtaning ma'nosini ko'rsatadigan muammo.

Faraz qilaylik, bizda 0 dan 10 gacha raqamlar ketma-ketligini tasodifiy oraliqda (0–n soniya) chop etadigan for tsikli bor. Va'dalarni ishlatib, raqamlar 0 dan 10 gacha ketma-ket chop etilishi uchun ushbu tsiklni o'zgartirishingiz kerak. Demak, nolni chop etish uchun 6 soniya va bittani chop etish uchun 2 soniya kerak bo'lsa, avval nol, keyin esa chop etilishi kerak. birini chop etish uchun ortga hisoblash boshlanadi.

Va, albatta, biz bu muammoni hal qilish uchun Async/Await yoki .sort dan foydalanmaymiz. Misol yechimi oxirida.

Async funktsiyalari

ES2017 (ES8) da asinxron funksiyalarning qo‘shilishi va’dalar bilan ishlash vazifasini soddalashtirdi. Shuni ta'kidlash kerakki, async funktsiyalari va'dalarning "ustida" ishlaydi. Bu funksiyalar sifat jihatidan farqli tushunchalarni ifodalamaydi. Async funktsiyalari va'dalarni ishlatadigan kodga muqobil sifatida mo'ljallangan.

Async/Await asinxron kod bilan ishlashni sinxron uslubda tashkil qilish imkonini beradi.

Shunday qilib, va'dalarni bilish Async/Await tamoyillarini tushunishni osonlashtiradi.

sintaktik

Odatda u ikkita kalit so'zdan iborat: async va wait. Birinchi so'z funksiyani asinxronga aylantiradi. Bunday funktsiyalar kutishdan foydalanishga imkon beradi. Boshqa har qanday holatda, ushbu funktsiyadan foydalanish xatolikka olib keladi.

// With function declaration
 
async function myFn() {
  // await ...
}
 
// With arrow function
 
const myFn = async () => {
  // await ...
}
 
function myFn() {
  // await fn(); (Syntax Error since no async)
}
 

Async funksiya deklaratsiyasining eng boshida, oʻq funksiyasi boʻlsa, “=” belgisi va qavslar orasiga kiritiladi.

Ushbu funktsiyalar ob'ektga usullar sifatida joylashtirilishi yoki sinf deklaratsiyasida ishlatilishi mumkin.

// 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! Shuni esda tutish kerakki, sinf konstruktorlari va qabul qiluvchilar/setterlar asinxron bo'lishi mumkin emas.

Semantika va ijro qoidalari

Async funktsiyalari asosan standart JS funktsiyalariga o'xshaydi, ammo istisnolar mavjud.

Shunday qilib, async funktsiyalari har doim va'dalarni qaytaradi:

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello

Xususan, fn salom satrini qaytaradi. Xo'sh, bu asinxron funksiya bo'lgani uchun, satr qiymati konstruktor yordamida va'daga o'raladi.

Mana Asyncsiz muqobil dizayn:

function fn() {
  return Promise.resolve('hello');
}
 
fn().then(console.log);
// hello

Bunday holda, va'da "qo'lda" qaytariladi. Asinxron funksiya har doim yangi va'da bilan o'ralgan.

Qaytish qiymati ibtidoiy bo'lsa, async funksiyasi qiymatni va'daga o'rash orqali qaytaradi. Qaytish qiymati va'da qilingan ob'ekt bo'lsa, uning o'lchamlari yangi va'dada qaytariladi.

const p = Promise.resolve('hello')
p instanceof Promise;
// true
 
Promise.resolve(p) === p;
// true
 

Ammo asinxron funksiya ichida xatolik bo'lsa nima bo'ladi?

async function foo() {
  throw Error('bar');
}
 
foo().catch(console.log);

Agar u qayta ishlanmasa, foo() va'dani rad etish bilan qaytaradi. Bunday holatda, Promise.resolve o'rniga xatoni o'z ichiga olgan Promise.reject qaytariladi.

Async funktsiyalari nima qaytarilishidan qat'i nazar, har doim va'da beradi.

Asenkron funktsiyalar har bir kutishda pauza qiladi.

Kutish ifodalarga ta'sir qiladi. Shunday qilib, agar ifoda va'da bo'lsa, va'da bajarilgunga qadar asinxronizatsiya funktsiyasi to'xtatiladi. Agar ifoda va'da bo'lmasa, u Promise.resolve orqali va'daga aylantiriladi va keyin yakunlanadi.

// 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);

Va bu erda fn funktsiyasi qanday ishlashining tavsifi.

  • Uni chaqirgandan so'ng, birinchi qator const a = await 9 dan aylantiriladi; in const a = kutish Promise.resolve(9);.
  • Await-dan foydalangandan so'ng, funktsiyaning bajarilishi a o'z qiymatini olguncha to'xtatiladi (hozirgi vaziyatda u 9).
  • delayAndGetRandom(1000) fn funksiyasining bajarilishini u o'zini tugatmaguncha (1 soniyadan keyin) to'xtatib turadi. Bu fn funksiyasini 1 soniya davomida samarali ravishda to'xtatadi.
  • delayAndGetRandom(1000) rezolyutsiya orqali tasodifiy qiymatni qaytaradi, keyin u b o'zgaruvchisiga tayinlanadi.
  • Xo'sh, c o'zgaruvchisi bo'lgan holat a o'zgaruvchisi bo'lgan holatga o'xshaydi. Shundan so'ng, hamma narsa bir soniya to'xtaydi, lekin endi delayAndGetRandom(1000) hech narsa qaytarmaydi, chunki bu talab qilinmaydi.
  • Natijada, qiymatlar a + b * c formulasi yordamida hisoblanadi. Natija Promise.resolve yordamida va'daga o'raladi va funksiya tomonidan qaytariladi.

Bu pauzalar ES6 dagi generatorlarni eslatishi mumkin, ammo bunda nimadir bor sizning sabablaringiz.

Muammoni hal qilish

Xo'sh, endi yuqorida aytib o'tilgan muammoning echimini ko'rib chiqaylik.

FinishMyTask funksiyasi queryDatabase, sendEmail, logTaskInFile va boshqalar kabi operatsiyalar natijalarini kutish uchun Await-dan foydalanadi. Agar siz ushbu yechimni va'dalar ishlatilgani bilan taqqoslasangiz, o'xshashliklar aniq bo'ladi. Biroq, Async/Await versiyasi barcha sintaktik murakkabliklarni sezilarli darajada soddalashtiradi. Bunday holda, .then/.catch kabi ko'p sonli qayta qo'ng'iroqlar va zanjirlar mavjud emas.

Mana, raqamlarning chiqishi bilan yechim, ikkita variant mavjud.

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);
    });
  });
};

Va bu erda async funktsiyalaridan foydalangan holda yechim mavjud.

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

Qayta ishlashda xatolik

Ishlov berilmagan xatolar rad etilgan va'daga o'raladi. Biroq, sinxronlash funktsiyalari xatolarni sinxronlashtirish uchun try/catch dan foydalanishi mumkin.

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() asinxron funksiya bo‘lib, u muvaffaqiyatli (“mukammal raqam”) yoki xato bilan (“Kechirasiz, raqam juda katta”) bajarilmaydi.

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

Yuqoridagi misol canRejectOrReturn ning bajarilishini kutganligi sababli, uning o'z ishlamay qolishi catch blokining bajarilishiga olib keladi. Natijada, foo funksiyasi aniqlanmagan (agar try blokida hech narsa qaytarilmasa) yoki ushlangan xato bilan tugaydi. Natijada, bu funktsiya muvaffaqiyatsiz bo'lmaydi, chunki try/catch foo funktsiyasini o'zi boshqaradi.

Mana yana bir misol:

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

Misolda canRejectOrReturn foo dan qaytarilganligiga e'tibor qaratish lozim. Foo bu holda mukammal raqam bilan tugaydi yoki Xato qaytaradi (“Kechirasiz, raqam juda katta”). Catch bloki hech qachon bajarilmaydi.

Muammo shundaki, foo canRejectOrReturn dan berilgan va'dani qaytaradi. Shunday qilib, foo ning yechimi canRejectOrReturn ning yechimiga aylanadi. Bunday holda, kod faqat ikkita qatordan iborat bo'ladi:

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

Agar kutish va qaytish ni birgalikda ishlatsangiz nima bo'ladi:

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

Yuqoridagi kodda foo mukammal raqam va xatolik bilan muvaffaqiyatli chiqadi. Bu erda hech qanday rad etish bo'lmaydi. Lekin foo undefined bilan emas, canRejectOrReturn bilan qaytadi. Qaytish kutish canRejectOrReturn() qatorini olib tashlash orqali bunga ishonch hosil qilaylik:

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

Umumiy xatolar va xatolar

Ba'zi hollarda Async/Await dan foydalanish xatolarga olib kelishi mumkin.

Unutilgan kutish

Bu tez-tez sodir bo'ladi - kutish kalit so'zi va'dadan oldin unutiladi:

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

Ko'rib turganingizdek, kodda kutish yoki qaytarish yo'q. Shuning uchun foo har doim 1 soniya kechikishsiz aniqlanmagan holda chiqadi. Ammo va'da amalga oshadi. Agar u xato yoki rad javobini bersa, UnhandledPromiseRejectionWarning chaqiriladi.

Qayta qo'ng'iroqlarda sinxronlash funktsiyalari

Async funktsiyalari ko'pincha .map yoki .filterda qayta qo'ng'iroqlar sifatida ishlatiladi. Misol tariqasida GitHub-dagi ochiq omborlar sonini qaytaruvchi fetchPublicReposCount(foydalanuvchi nomi) funksiyasini keltirish mumkin. Aytaylik, uchta foydalanuvchi bor, ularning ko'rsatkichlari bizga kerak. Mana bu vazifa uchun kod:

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

Bizga ArfatSalman, oktokat, norvig hisoblari kerak. Bunday holda biz:

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

.map qayta qo'ng'iroq qilishda Await-ga e'tibor qaratish lozim. Bu erda hisoblar va'dalar to'plamidir va .map har bir ko'rsatilgan foydalanuvchi uchun anonim qayta qo'ng'iroqdir.

Kutishdan haddan tashqari doimiy foydalanish

Misol tariqasida ushbu kodni olaylik:

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

Bu erda repo raqami count o'zgaruvchisiga joylashtiriladi, keyin bu raqam counts massiviga qo'shiladi. Kod bilan bog'liq muammo shundaki, birinchi foydalanuvchining ma'lumotlari serverdan kelgunga qadar, barcha keyingi foydalanuvchilar kutish rejimida bo'ladi. Shunday qilib, bir vaqtning o'zida faqat bitta foydalanuvchi qayta ishlanadi.

Agar, masalan, bitta foydalanuvchini qayta ishlash uchun taxminan 300 milodiy vaqt kerak bo'lsa, barcha foydalanuvchilar uchun bu allaqachon bir soniya; sarflangan vaqt lineer ravishda foydalanuvchilar soniga bog'liq. Ammo repo sonini olish bir-biriga bog'liq emasligi sababli, jarayonlarni parallellashtirish mumkin. Bu .map va Promise.all bilan ishlashni talab qiladi:

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

Promise.all kirish sifatida bir qator va'dalarni oladi va va'dani qaytaradi. Ikkinchisi, massivdagi barcha va'dalar bajarilgandan so'ng yoki birinchi rad etishda bajariladi. Ularning barchasi bir vaqtning o'zida boshlamasligi mumkin - bir vaqtning o'zida ishga tushirishni ta'minlash uchun siz p-map-dan foydalanishingiz mumkin.

xulosa

Async funktsiyalari rivojlanish uchun tobora muhim ahamiyat kasb etmoqda. Xo'sh, async funktsiyalaridan moslashuvchan foydalanish uchun siz foydalanishingiz kerak Async iteratorlar. JavaScript dasturchisi buni yaxshi bilishi kerak.

Skillbox tavsiya qiladi:

Manba: www.habr.com

a Izoh qo'shish