Við skulum skoða Async/Await í JavaScript með því að nota dæmi

Höfundur greinarinnar skoðar dæmi um Async/Await í JavaScript. Á heildina litið er Async/Await þægileg leið til að skrifa ósamstilltan kóða. Áður en þessi eiginleiki birtist var slíkur kóði skrifaður með því að nota svarhringingar og loforð. Höfundur upprunalegu greinarinnar afhjúpar kosti Async/Await með því að greina ýmis dæmi.

Við minnum á: fyrir alla Habr lesendur - 10 rúblur afsláttur þegar þú skráir þig á hvaða Skillbox námskeið sem er með því að nota Habr kynningarkóðann.

Skillbox mælir með: Fræðslunámskeið á netinu "Java verktaki".

Svarhringingu

Tilbakahringing er aðgerð þar sem símtalinu er seinkað um óákveðinn tíma. Áður voru hringingar notaðar á þeim sviðum kóðans þar sem ekki var hægt að fá niðurstöðu strax.

Hér er dæmi um ósamstillt lestur á skrá í Node.js:

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

Vandamál koma upp þegar þú þarft að framkvæma nokkrar ósamstilltar aðgerðir í einu. Við skulum ímynda okkur þessa atburðarás: beiðni er send til Arfat notendagagnagrunnsins, þú þarft að lesa profile_img_url reitinn hans og hlaða niður mynd af someserver.com þjóninum.
Eftir niðurhal breytum við myndinni í annað snið, til dæmis úr PNG í JPEG. Ef umbreytingin tókst er bréf sent á netfang notandans. Næst eru upplýsingar um atburðinn færðar inn í transformations.log skrána sem gefur til kynna dagsetninguna.

Það er þess virði að borga eftirtekt til skörun svarhringinga og fjölda }) í síðasta hluta kóðans. Það heitir Callback Hell eða Pyramid of Doom.

Ókostirnir við þessa aðferð eru augljósir:

  • Það er erfitt að lesa þennan kóða.
  • Það er líka erfitt að höndla villur, sem oft leiðir til lélegra kóðagæða.

Til að leysa þetta vandamál var loforðum bætt við JavaScript. Þær gera þér kleift að skipta út djúpum hreiðurhringingum fyrir orðið .þá.

Jákvæð hlið loforða er að þau gera kóðann miklu læsilegri, frá toppi til botns frekar en frá vinstri til hægri. Hins vegar hafa loforð einnig sín vandamál:

  • Þú þarft að bæta við miklu af .þá.
  • Í stað try/catch er .catch notað til að meðhöndla allar villur.
  • Það er ekki alltaf þægilegt að vinna með mörg loforð innan einni lykkju; í sumum tilfellum flækja þau kóðann.

Hér er vandamál sem mun sýna merkingu síðasta atriðisins.

Segjum að við höfum for-lykkju sem prentar talnaröð frá 0 til 10 með tilviljunarkenndu millibili (0–n sekúndur). Með því að nota loforð þarftu að breyta þessari lykkju þannig að tölurnar séu prentaðar í röð frá 0 í 10. Þannig að ef það tekur 6 sekúndur að prenta núll og 2 sekúndur að prenta eitt, ætti núllið að vera prentað fyrst og síðan niðurtalning fyrir prentun þann mun hefjast.

Og auðvitað notum við ekki Async/Await eða .sort til að leysa þetta vandamál. Dæmi um lausn er í lokin.

Ósamstilltur aðgerðir

Að bæta við ósamstilltu aðgerðum í ES2017 (ES8) einfaldaði það verkefni að vinna með loforð. Ég tek fram að ósamstilltar aðgerðir virka „fyrir ofan“ loforð. Þessar aðgerðir tákna ekki eigindlega ólík hugtök. Ósamstilltur aðgerðir eru ætlaðar sem valkostur við kóða sem notar loforð.

Async/Await gerir það mögulegt að skipuleggja vinnu með ósamstilltan kóða í samstilltum stíl.

Þannig að vita loforð gerir það auðveldara að skilja meginreglur Async/Await.

Setningafræði

Venjulega samanstendur það af tveimur leitarorðum: ósamstilltur og bíða. Fyrsta orðið breytir aðgerðinni í ósamstillt. Slíkar aðgerðir leyfa notkun bíða. Í öllum öðrum tilfellum mun notkun þessa aðgerð mynda villu.

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

Ósamstilling er sett inn í byrjun fallayfirlýsingarinnar, og ef um er að ræða örfall, á milli „=“ táknsins og sviga.

Þessar aðgerðir er hægt að setja í hlut sem aðferðir eða nota í flokksyfirlýsingu.

// 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! Það er þess virði að muna að flokkssmiðir og getters/setrar geta ekki verið ósamstilltir.

Merkingarfræði og framkvæmdarreglur

Ósamstilltur aðgerðir eru í grundvallaratriðum svipaðar venjulegum JS aðgerðir, en það eru undantekningar.

Þannig skila ósamstilltar aðgerðir alltaf loforð:

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

Nánar tiltekið, fn skilar strengnum halló. Jæja, þar sem þetta er ósamstillt fall, þá er strengjagildið vafið inn í loforð með því að nota smiði.

Hér er önnur hönnun án Async:

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

Í þessu tilviki er loforðinu skilað „handvirkt“. Ósamstillt fall er alltaf pakkað inn í nýtt loforð.

Ef skilagildið er frumstætt, skilar ósamstillingarfallið gildinu með því að pakka því inn í loforð. Ef skilagildið er loforðshlutur er upplausn hans skilað í nýju loforði.

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

En hvað gerist ef það er villa í ósamstilltri aðgerð?

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

Ef það er ekki unnið mun foo() skila loforði með höfnun. Í þessum aðstæðum verður Promise.reject sem inniheldur villu skilað í stað Promise.resolve.

Ósamstilltur aðgerðir gefa alltaf út loforð, óháð því hvað er skilað.

Ósamstilltur aðgerðir gera hlé á hverri bið.

Bíða hefur áhrif á tjáningu. Þannig að ef tjáningin er loforð er ósamstillingaraðgerðinni stöðvað þar til loforðið er uppfyllt. Ef tjáningin er ekki loforð er henni breytt í loforð í gegnum Promise.resolve og síðan lokið.

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

Og hér er lýsing á því hvernig fn aðgerðin virkar.

  • Eftir að hafa kallað hana er fyrstu línunni breytt úr const a = await 9; in const a = await Promise.resolve(9);.
  • Eftir að Await hefur verið notað er framkvæmd aðgerðarinnar stöðvuð þar til a fær gildi sitt (í núverandi ástandi er það 9).
  • delayAndGetRandom(1000) gerir hlé á framkvæmd fn fallsins þar til hún lýkur sjálfri sér (eftir 1 sekúndu). Þetta stöðvar fn aðgerðina í raun í 1 sekúndu.
  • delayAndGetRandom(1000) via resolve skilar slembigildi, sem síðan er úthlutað breytunni b.
  • Jæja, málið með breytu c er svipað og með breytu a. Eftir það stoppar allt í eina sekúndu, en núna skilar delayAndGetRandom(1000) engu vegna þess að það er ekki krafist.
  • Þess vegna eru gildin reiknuð út með formúlunni a + b * c. Niðurstaðan er vafin inn í loforð með því að nota Promise.resolve og skilað af aðgerðinni.

Þessar pásur kunna að minna á rafala í ES6, en það er eitthvað til í því ástæður þínar.

Að leysa vandamálið

Jæja, nú skulum við líta á lausnina á vandamálinu sem nefnt er hér að ofan.

FinishMyTask aðgerðin notar Await til að bíða eftir niðurstöðum aðgerða eins og queryDatabase, sendEmail, logTaskInFile og fleiri. Ef þú berð þessa lausn saman við þá þar sem loforð voru notuð, verða líkindin augljós. Hins vegar einfaldar Async/Await útgáfan til muna alla setningafræðilega margbreytileika. Í þessu tilviki er enginn fjöldi svarhringinga og keðja eins og .þá/.fanga.

Hér er lausn með úttak af tölum, það eru tveir valkostir.

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

Og hér er lausn sem notar ósamstillingaraðgerðir.

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

Villa við vinnslu

Ómeðhöndlaðar villur eru pakkaðar inn í hafnað loforð. Hins vegar geta ósamstilltar aðgerðir notað try/catch til að meðhöndla villur samstillt.

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() er ósamstillt fall sem annað hvort tekst („fullkomið númer“) eða mistekst með villu („Því miður, tala of stór“).

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

Þar sem dæmið hér að ofan gerir ráð fyrir að canRejectOrReturn verði keyrt, mun eigin bilun leiða til framkvæmdar aflablokkarinnar. Fyrir vikið mun aðgerðin foo enda annað hvort með óskilgreindu (þegar engu er skilað í tilraunablokkinni) eða með villu sem náðist. Þess vegna mun þessi aðgerð ekki mistakast vegna þess að try/catch mun sjá um aðgerðina foo sjálfa.

Hér er annað dæmi:

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

Það er þess virði að borga eftirtekt til þess að í dæminu er canRejectOrReturn skilað frá foo. Foo í þessu tilfelli endar annað hvort með fullkominni tölu eða skilar villu ("Því miður, tala of stór"). Aflablokkin verður aldrei framkvæmd.

Vandamálið er að foo skilar loforðinu sem gefið var frá canRejectOrReturn. Þannig að lausnin við foo verður lausnin við canRejectOrReturn. Í þessu tilviki mun kóðinn aðeins samanstanda af tveimur línum:

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

Hér er það sem gerist ef þú notar bíða og snúa aftur saman:

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

Í kóðanum hér að ofan mun foo hætta með góðum árangri með bæði fullkomið númer og villu gripin. Hér verður ekki neitað. En foo mun koma aftur með canRejectOrReturn, ekki með undefined. Við skulum ganga úr skugga um þetta með því að fjarlægja return await canRejectOrReturn() línuna:

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

Algeng mistök og gildrur

Í sumum tilfellum getur notkun Async/Await leitt til villna.

Gleymt bíða

Þetta gerist nokkuð oft - bíður lykilorðið gleymist á undan loforðið:

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

Eins og þú sérð er engin bið eða endurkoma í kóðanum. Þess vegna fer foo alltaf út með óskilgreindum án 1 sekúndu seinkun. En loforðið verður efnt. Ef það veldur villu eða höfnun verður UnhandledPromiseRejectionWarning kallað.

Ósamstilltur aðgerðir í svarhringingum

Ósamstilltur aðgerðir eru nokkuð oft notaðar í .map eða .filter sem svarhringingar. Dæmi er fetchPublicReposCount(notendanafn) fallið, sem skilar fjölda opinna geymsla á GitHub. Segjum að það séu þrír notendur sem við þurfum að hafa mæligildi fyrir. Hér er kóðinn fyrir þetta verkefni:

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

Við þurfum ArfatSalman, octocat, norvig reikninga. Í þessu tilfelli gerum við:

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

Það er þess virði að borga eftirtekt til Await í .map svarhringingunni. Hér gildir fjöldi loforða og .map er nafnlaust svarhringingu fyrir hvern tiltekinn notanda.

Of stöðug notkun á bíða

Tökum þennan kóða sem dæmi:

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

Hér er endurhverfunúmerið sett í talningarbreytuna, síðan er þessari tölu bætt við talningarfylkinguna. Vandamálið með kóðann er að þar til gögn fyrsta notandans koma frá þjóninum verða allir síðari notendur í biðham. Þannig er aðeins einn notandi afgreiddur í einu.

Ef það tekur til dæmis um 300 ms að vinna úr einum notanda, þá er það nú þegar sekúnda fyrir alla notendur; tíminn sem varið er línulega fer eftir fjölda notenda. En þar sem að fá fjölda endurhverfa er ekki háð hvert öðru, þá er hægt að samsíða ferlunum. Þetta krefst þess að vinna með .map og 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 fær fjölda loforða sem inntak og skilar loforði. Hið síðarnefnda, eftir að öll loforð í fylkinu hafa lokið eða við fyrstu höfnun, er lokið. Það getur gerst að þeir ræsist ekki allir á sama tíma - til að tryggja samtímis byrjun er hægt að nota p-map.

Ályktun

Ósamstilltur aðgerðir verða sífellt mikilvægari fyrir þróun. Jæja, til aðlagandi notkunar á ósamstillingaraðgerðum ættirðu að nota Ósamstilltir endurtekningar. JavaScript forritari ætti að vera vel kunnugur þessu.

Skillbox mælir með:

Heimild: www.habr.com

Bæta við athugasemd