Wacha tuangalie Async/Subiri kwenye JavaScript kwa kutumia mifano

Mwandishi wa makala anachunguza mifano ya Async/Await katika JavaScript. Kwa ujumla, Async/Await ni njia rahisi ya kuandika msimbo wa asynchronous. Kabla ya kipengele hiki kuonekana, nambari kama hiyo iliandikwa kwa kutumia simu na ahadi. Mwandishi wa makala asilia anaonyesha faida za Async/Ait kwa kuchanganua mifano mbalimbali.

Tunakukumbusha: kwa wasomaji wote wa "Habr" - punguzo la rubles 10 wakati wa kujiandikisha katika kozi yoyote ya Skillbox kwa kutumia msimbo wa uendelezaji wa "Habr".

Skillbox inapendekeza: Kozi ya elimu mtandaoni "Msanidi programu wa Java".

Callback

Kurudisha nyuma ni chaguo la kukokotoa ambalo simu yake imechelewa kwa muda usiojulikana. Hapo awali, simu za nyuma zilitumiwa katika maeneo hayo ya msimbo ambapo matokeo hayakuweza kupatikana mara moja.

Hapa kuna mfano wa kusoma faili kwa usawa katika Node.js:

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

Matatizo hutokea wakati unahitaji kufanya shughuli kadhaa za asynchronous mara moja. Hebu tufikirie hali hii: ombi linatumwa kwa hifadhidata ya mtumiaji wa Arfat, unahitaji kusoma sehemu yake ya profile_img_url na kupakua picha kutoka kwa seva ya someserver.com.
Baada ya kupakua, tunabadilisha picha kwenye muundo mwingine, kwa mfano kutoka kwa PNG hadi JPEG. Ikiwa ubadilishaji ulifanikiwa, barua hutumwa kwa barua pepe ya mtumiaji. Ifuatayo, habari kuhusu tukio huingizwa kwenye faili ya transformations.log, inayoonyesha tarehe.

Inafaa kulipa kipaumbele kwa mwingiliano wa simu na idadi kubwa ya }) katika sehemu ya mwisho ya nambari. Inaitwa Kuzimu ya Kurudi nyuma au Piramidi ya Adhabu.

Ubaya wa njia hii ni dhahiri:

  • Nambari hii ni ngumu kusoma.
  • Pia ni vigumu kushughulikia makosa, ambayo mara nyingi husababisha ubora duni wa msimbo.

Ili kutatua tatizo hili, ahadi ziliongezwa kwa JavaScript. Zinakuruhusu kubadilisha nesting ya kina ya callbacks na neno .basi.

Kipengele chanya cha ahadi ni kwamba hufanya msimbo kusomeka vizuri zaidi, kutoka juu hadi chini badala ya kutoka kushoto kwenda kulia. Walakini, ahadi pia zina shida zao:

  • Unahitaji kuongeza mengi ya .basi.
  • Badala ya kujaribu/kukamata, .catch inatumika kushughulikia hitilafu zote.
  • Kufanya kazi na ahadi nyingi ndani ya kitanzi kimoja sio rahisi kila wakati; katika hali zingine, huchanganya nambari.

Hapa kuna shida ambayo itaonyesha maana ya nukta ya mwisho.

Tuseme tuna kitanzi ambacho huchapisha mlolongo wa nambari kutoka 0 hadi 10 kwa vipindi nasibu (sekunde 0–n). Kwa kutumia ahadi, unahitaji kubadilisha kitanzi hiki ili nambari zichapishwe kwa mlolongo kutoka 0 hadi 10. Kwa hiyo, ikiwa inachukua sekunde 6 kuchapisha sifuri na sekunde 2 ili kuchapisha moja, sifuri inapaswa kuchapishwa kwanza, na kisha. siku iliyosalia ya kuchapisha itaanza.

Na bila shaka, hatutumii Async/Await au .sort kutatua tatizo hili. Suluhisho la mfano liko mwishoni.

Vitendaji vya Async

Kuongezwa kwa kazi za async katika ES2017 (ES8) kumerahisisha kazi ya kufanya kazi kwa ahadi. Ninagundua kuwa kazi za async hufanya kazi "juu" ya ahadi. Kazi hizi haziwakilishi dhana tofauti kimaelezo. Vitendaji vya Async vimekusudiwa kama njia mbadala ya msimbo unaotumia ahadi.

Async/Await hufanya iwezekane kupanga kazi kwa kutumia msimbo wa asynchronous katika mtindo wa kusawazisha.

Kwa hivyo, kujua ahadi hurahisisha kuelewa kanuni za Async/Await.

syntax

Kwa kawaida huwa na maneno mawili muhimu: async na await. Neno la kwanza hugeuza kitendakazi kuwa kisichosawazisha. Kazi hizo huruhusu matumizi ya kusubiri. Katika hali nyingine yoyote, kutumia chaguo hili kutatoa hitilafu.

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

Async huwekwa mwanzoni kabisa mwa tamko la chaguo la kukokotoa, na katika hali ya chaguo za kukokotoa za kishale, kati ya ishara ya "=" na mabano.

Vitendaji hivi vinaweza kuwekwa kwenye kitu kama mbinu au kutumika katika tamko la darasa.

// 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! Inafaa kukumbuka kuwa wajenzi wa darasa na watengenezaji / wawekaji hawawezi kuwa sawa.

Semantiki na sheria za utekelezaji

Vitendaji vya Async kimsingi vinafanana na vitendaji vya kawaida vya JS, lakini kuna vighairi.

Kwa hivyo, kazi za async kila wakati hurudisha ahadi:

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

Hasa, fn inarudisha kamba hujambo. Naam, kwa kuwa hii ni kazi isiyo ya kawaida, thamani ya kamba imefungwa kwa ahadi kwa kutumia mjenzi.

Hapa kuna muundo mbadala bila Async:

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

Katika kesi hii, ahadi inarejeshwa "kwa mikono". Utendakazi wa asynchronous daima umefungwa katika ahadi mpya.

Ikiwa thamani ya kurejesha ni ya kwanza, chaguo la kukokotoa la async hurejesha thamani kwa kuifunga kwa ahadi. Ikiwa thamani ya kurudi ni kitu cha ahadi, azimio lake linarejeshwa katika ahadi mpya.

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

Lakini ni nini hufanyika ikiwa kuna hitilafu ndani ya kazi ya asynchronous?

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

Ikiwa haijachakatwa, foo() itarudisha ahadi kwa kukataliwa. Katika hali hii, Promise.reject iliyo na hitilafu itarejeshwa badala ya Promise.resolve.

Kazi za Async daima hutoa ahadi, bila kujali ni nini kinachorejeshwa.

Vitendaji vya Asynchronous husitisha kila await .

Subiri huathiri misemo. Kwa hivyo, ikiwa usemi ni ahadi, utendakazi wa usawazishaji umesimamishwa hadi ahadi itimizwe. Ikiwa usemi si ahadi, hubadilishwa kuwa ahadi kupitia Promise.resolve na kisha kukamilishwa.

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

Na hapa kuna maelezo ya jinsi kazi ya fn inavyofanya kazi.

  • Baada ya kuiita, mstari wa kwanza unabadilishwa kutoka const a = kusubiri 9; in const a = subiri Promise.resolve(9);.
  • Baada ya kutumia Await, utekelezaji wa kazi umesimamishwa hadi a kupata thamani yake (katika hali ya sasa ni 9).
  • delayAndGetRandom(1000) inasitisha utekelezaji wa kazi ya fn hadi ikamilishe yenyewe (baada ya sekunde 1). Hii inasimamisha kazi ya fn kwa sekunde 1.
  • delayAndGetRandom(1000) kupitia resolution hurejesha thamani nasibu, ambayo huwekwa kwa kutofautisha b.
  • Kweli, kesi iliyo na kutofautisha c ni sawa na kesi yenye kutofautisha a. Baada ya hapo, kila kitu kinasimama kwa sekunde, lakini sasa delayAndGetRandom(1000) hairudishi chochote kwa sababu haihitajiki.
  • Kama matokeo, maadili huhesabiwa kwa kutumia formula a + b * c. Matokeo yake yamefungwa kwa ahadi kwa kutumia Promise.resolve na kurudishwa na chaguo la kukokotoa.

Usitishaji huu unaweza kuwa ukumbusho wa jenereta katika ES6, lakini kuna jambo kwa hilo sababu zako.

Kutatua tatizo

Naam, sasa hebu tuangalie suluhisho la tatizo lililotajwa hapo juu.

Kitendaji cha finishMyTask hutumia Await kusubiri matokeo ya shughuli kama vile queryDatabase, sendEmail, logTaskInFile, na zingine. Ikiwa unalinganisha suluhisho hili na lile ambalo ahadi zilitumiwa, kufanana kutakuwa dhahiri. Walakini, toleo la Async/Await hurahisisha ugumu wote wa kisintaksia. Katika hali hii, hakuna idadi kubwa ya simu na minyororo kama .then/.catch.

Hapa kuna suluhisho na matokeo ya nambari, kuna chaguzi mbili.

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

Na hapa kuna suluhisho kwa kutumia kazi za async.

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

Hitilafu katika kuchakata

Makosa ambayo hayajashughulikiwa yamefungwa katika ahadi iliyokataliwa. Hata hivyo, utendakazi wa async unaweza kutumia try/catch kushughulikia hitilafu kwa usawazishaji.

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() ni chaguo la kukokotoa lisilosawazisha ambalo linaweza kufaulu (β€œnambari kamili”) au kushindwa kwa hitilafu (β€œSamahani, nambari ni kubwa mno”).

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

Kwa kuwa mfano hapo juu unatarajia canRejectOrReturn kutekeleza, kutofaulu kwake kutasababisha utekelezaji wa kizuizi cha kukamata. Kama matokeo, chaguo la kukokotoa litaisha na ama isiyofafanuliwa (wakati hakuna kitakachorejeshwa kwenye kizuizi cha kujaribu) au kwa hitilafu iliyopatikana. Kama matokeo, chaguo hili la kukokotoa halitashindwa kwa sababu kujaribu/kukamata kutashughulikia chaguo la kukokotoa la foo lenyewe.

Hapa kuna mfano mwingine:

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

Inafaa kuzingatia ukweli kwamba kwa mfano, canRejectOrReturn inarudishwa kutoka foo. Foo katika kesi hii huisha na nambari kamili au huleta Hitilafu ("Samahani, nambari ni kubwa sana"). Kizuizi cha kukamata hakitawahi kutekelezwa.

Shida ni kwamba foo inarudisha ahadi iliyopitishwa kutoka kwa canRejectOrReturn. Kwa hivyo suluhisho la foo linakuwa suluhisho la canRejectOrReturn. Katika kesi hii, nambari itakuwa na mistari miwili tu:

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

Hivi ndivyo kitakachotokea ikiwa utatumia await na kurudi pamoja:

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

Katika nambari iliyo hapo juu, foo itatoka kwa mafanikio ikiwa na nambari kamili na hitilafu iliyopatikana. Hakutakuwa na kukataa hapa. Lakini foo itarudi na canRejectOrReturn, sio isiyofafanuliwa. Wacha tuhakikishe hii kwa kuondoa mstari wa kurudi await canRejectOrReturn():

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

Makosa ya kawaida na mitego

Katika baadhi ya matukio, kutumia Async/Await kunaweza kusababisha makosa.

Umesahau kusubiri

Hii hufanyika mara nyingi - neno kuu la kungojea limesahaulika kabla ya ahadi:

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

Kama unaweza kuona, hakuna kungojea au kurudi kwenye nambari. Kwa hivyo foo kila wakati hutoka bila kufafanuliwa bila kucheleweshwa kwa sekunde 1. Lakini ahadi itatimizwa. Ikitupa hitilafu au kukataliwa, basi UnhandledPromiseRejectionWarning itaitwa.

Async Kazi katika Callbacks

Vitendaji vya Async mara nyingi hutumika katika .map au .chujio kama virudishi nyuma. Mfano ni chaguo la kukokotoa la fetchPublicReposCount(jina la mtumiaji), ambalo hurejesha idadi ya hazina zilizo wazi kwenye GitHub. Hebu tuseme kuna watumiaji watatu ambao tunahitaji vipimo vyao. Hapa kuna nambari ya kazi hii:

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

Tunahitaji akaunti za ArfatSalman, octocat, norvig. Katika kesi hii, tunafanya:

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

Inafaa kulipa kipaumbele kwa Subiri katika .map callback. Hapa hesabu ni safu ya ahadi, na .map ni upigaji simu usiojulikana kwa kila mtumiaji aliyebainishwa.

Matumizi thabiti kupita kiasi ya kusubiri

Wacha tuchukue nambari hii kama mfano:

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

Hapa nambari ya repo imewekwa katika kutofautisha kwa hesabu, kisha nambari hii inaongezwa kwa safu ya hesabu. Shida na nambari ni kwamba hadi data ya mtumiaji wa kwanza ifike kutoka kwa seva, watumiaji wote wanaofuata watakuwa katika hali ya kusubiri. Kwa hivyo, mtumiaji mmoja tu ndiye anayechakatwa kwa wakati mmoja.

Ikiwa, kwa mfano, inachukua takriban 300 ms kuchakata mtumiaji mmoja, basi kwa watumiaji wote tayari ni sekunde; wakati unaotumika kwa mstari unategemea idadi ya watumiaji. Lakini kwa kuwa kupata idadi ya repo haitegemei kila mmoja, michakato inaweza kusawazishwa. Hii inahitaji kufanya kazi na .map na 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 inapokea safu ya ahadi kama ingizo na inarudisha ahadi. Mwisho, baada ya ahadi zote katika safu kukamilika au kwa kukataa kwanza, imekamilika. Inaweza kutokea kwamba wote hawaanza kwa wakati mmoja - ili kuhakikisha kuanza kwa wakati mmoja, unaweza kutumia p-map.

Hitimisho

Vitendaji vya Async vinazidi kuwa muhimu kwa usanidi. Kweli, kwa utumiaji mzuri wa vitendaji vya async, unapaswa kutumia Async Iterators. Msanidi programu wa JavaScript anapaswa kuwa mjuzi katika hili.

Skillbox inapendekeza:

Chanzo: mapenzi.com

Kuongeza maoni