Litte wy nei Async / Await sjen yn JavaScript mei foarbylden

De skriuwer fan it artikel ûndersiket foarbylden fan Async / Wachtsje yn JavaScript. Oer it algemien is Async / Await in handige manier om asynchrone koade te skriuwen. Foardat dizze funksje ferskynde, waard sa'n koade skreaun mei callbacks en beloften. De skriuwer fan it orizjinele artikel ûntbleatet de foardielen fan Async / Wachtsje troch ferskate foarbylden te analysearjen.

Wy herinnerje: foar alle lêzers fan "Habr" - in koarting fan 10 roebel by it ynskriuwen fan in Skillbox-kursus mei de promoasjekoade "Habr".

Skillbox advisearret: Edukative online kursus "Java-ûntwikkelder".

CallBack

Callback is in funksje wêrfan de oprop foar ûnbepaalde tiid wurdt fertrage. Eartiids waarden callbacks brûkt yn dy gebieten fan koade dêr't it resultaat net direkt te krijen koe.

Hjir is in foarbyld fan it asynchronysk lêzen fan in bestân yn Node.js:

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

Problemen ûntsteane as jo ferskate asynchrone operaasjes tagelyk moatte útfiere. Litte wy ús dit senario foarstelle: in fersyk wurdt dien oan de Arfat-brûkersdatabase, jo moatte it fjild profile_img_url lêze en in ôfbylding downloade fan 'e someserver.com-tsjinner.
Nei it ynladen konvertearje wy de ôfbylding nei in oar formaat, bygelyks fan PNG nei JPEG. As de konverzje suksesfol wie, wurdt in brief stjoerd nei de e-post fan de brûker. Folgjende wurdt ynformaasje oer it evenemint ynfierd yn it transformations.log-bestân, wêrby't de datum oanjout.

It is it wurdich omtinken te jaan oan de oerlaap fan callbacks en it grutte oantal }) yn it lêste diel fan 'e koade. It hjit Callback Hell of Pyramid of Doom.

De neidielen fan dizze metoade binne dúdlik:

  • Dizze koade is lestich te lêzen.
  • It is ek lestich om flaters te behanneljen, wat faaks liedt ta minne koadekwaliteit.

Om dit probleem op te lossen, waarden beloften tafoege oan JavaScript. Se tastean jo te ferfangen djippe nêst fan callbacks mei it wurd .dan.

It positive aspekt fan beloften is dat se de koade folle better lêsber meitsje, fan boppen nei ûnderen as fan links nei rjochts. Beloften hawwe lykwols ek har problemen:

  • Jo moatte in protte .dan tafoegje.
  • Ynstee fan try/catch, wurdt .catch brûkt om alle flaters te behanneljen.
  • Wurkje mei meardere beloften binnen ien lus is net altyd handich yn guon gefallen, se komplisearje de koade.

Hjir is in probleem dat sil sjen litte de betsjutting fan it lêste punt.

Stel dat wy in for-lus hawwe dy't in sekwinsje fan nûmers fan 0 oant 10 printsje mei willekeurige yntervallen (0-n sekonden). Mei help fan beloften moatte jo dizze loop feroarje, sadat de nûmers yn folchoarder wurde printe fan 0 oant 10. Dus, as it 6 sekonden duorret om in nul te printsjen en 2 sekonden om ien te printsjen, moat de nul earst printe wurde, en dan it ôftellen foar it printsjen fan de iene sil begjinne.

En fansels brûke wy gjin Async / Wachtsje of .sort om dit probleem op te lossen. In foarbyld oplossing is oan 'e ein.

Async funksjes

De tafoeging fan asyncfunksjes yn ES2017 (ES8) ferienfâldige de taak om te wurkjen mei beloften. Ik konstatearje dat asyncfunksjes "boppe" fan beloften wurkje. Dizze funksjes fertsjintwurdigje gjin kwalitatyf ferskillende begripen. Async-funksjes binne bedoeld as alternatyf foar koade dy't beloften brûkt.

Async / Wachtsje makket it mooglik om wurk te organisearjen mei asynchrone koade yn in syngroane styl.

Sa, it witten fan beloften makket it makliker om de prinsipes fan Async / Await te begripen.

syntaksis

Normaal bestiet it út twa kaaiwurden: async en wachtsje. It earste wurd feroaret de funksje yn asynchrone. Sokke funksjes tastean it brûken fan wachtsje. Yn alle oare gefallen sil it brûken fan dizze funksje in flater generearje.

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

Async wurdt ynfoege oan it begjin fan 'e funksjedeklaraasje, en yn it gefal fan in pylkfunksje, tusken it teken "=" en de haakjes.

Dizze funksjes kinne wurde pleatst yn in objekt as metoaden of brûkt yn in klasse deklaraasje.

// 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! It is de muoite wurdich om te ûnthâlden dat klasse constructors en getters / setters kin net wêze asynchronous.

Semantyk en útfieringsregels

Async-funksjes binne yn prinsipe fergelykber mei standert JS-funksjes, mar d'r binne útsûnderingen.

Sa, async-funksjes jouwe altyd beloften:

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

Spesifyk jout fn de tekenrige hello werom. No, om't dit in asynchrone funksje is, wurdt de tekenrige wearde ferpakt yn in belofte mei in konstruktor.

Hjir is in alternatyf ûntwerp sûnder Async:

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

Yn dit gefal wurdt de belofte "hânmjittich" weromjûn. In asynchrone funksje is altyd ferpakt yn in nije belofte.

As de weromwearde in primityf is, jout de async-funksje de wearde werom troch it yn in belofte te wikkeljen. As de weromwearde in belofte-objekt is, wurdt syn resolúsje weromjûn yn in nije belofte.

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

Mar wat bart der as d'r in flater is yn in asynchrone funksje?

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

As it net wurdt ferwurke, sil foo () in belofte weromkomme mei ôfwizing. Yn dizze situaasje wurdt Promise.reject mei in flater weromjûn ynstee fan Promise.resolve.

Async-funksjes jouwe altyd in belofte út, nettsjinsteande wat wurdt weromjûn.

Asynchrone funksjes pauze op elk wachtsjen.

Wachtsje beynfloedet útdrukkingen. Dus, as de útdrukking in belofte is, wurdt de asyncfunksje ophâlden oant de belofte is folbrocht. As de útdrukking gjin belofte is, wurdt it omboud ta in belofte fia Promise.resolve en dan foltôge.

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

En hjir is in beskriuwing fan hoe't de fn-funksje wurket.

  • Nei it oproppen wurdt de earste rigel omsetten fan const a = await 9; in const a = await Promise.resolve(9);.
  • Nei it brûken fan Wachtsje, wurdt de útfiering fan de funksje ophâlden oant a syn wearde krijt (yn de hjoeddeistige situaasje is it 9).
  • delayAndGetRandom(1000) stopet de útfiering fan de fn-funksje oant it himsels foltôget (nei 1 sekonde). Dit stopet de fn-funksje effektyf foar 1 sekonde.
  • delayAndGetRandom(1000) fia resolve jout in willekeurige wearde werom, dy't dan wurdt tawiisd oan de fariabele b.
  • No, it gefal mei fariabele c is fergelykber mei it gefal mei fariabele a. Dêrnei hâldt alles foar in sekonde op, mar no jout delayAndGetRandom(1000) neat werom, om't it net nedich is.
  • As resultaat wurde de wearden berekkene mei de formule a + b * c. It resultaat wurdt ferpakt yn in belofte mei Promise.resolve en weromjûn troch de funksje.

Dizze pauzes kinne tinke oan generators yn ES6, mar d'r is wat oan dyn redenen.

It oplossen fan it probleem

No, litte wy no sjen nei de oplossing foar it hjirboppe neamde probleem.

De finishMyTask-funksje brûkt Await om te wachtsjen op de resultaten fan operaasjes lykas queryDatabase, sendEmail, logTaskInFile, en oaren. As jo ​​​​dizze oplossing fergelykje mei dejinge wêr't beloften waarden brûkt, sille de oerienkomsten dúdlik wurde. De Async / Await-ferzje ferienfâldigt lykwols alle syntaktyske kompleksiteiten gâns. Yn dit gefal is der gjin grut oantal callbacks en keatlingen lykas .then/.catch.

Hjir is in oplossing mei de útfier fan nûmers, der binne twa opsjes.

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

En hjir is in oplossing mei asyngronisaasjefunksjes.

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

Flater ferwurking

Unbehandele flaters wurde ferpakt yn in ôfwiisde belofte. Async-funksjes kinne lykwols besykje / fangen brûke om flaters synchroon te behanneljen.

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 () is in asynchrone funksje dy't of slagget ("perfekt nûmer") of mislearret mei in flater ("Sorry, nûmer te grut").

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

Sûnt it foarbyld hjirboppe ferwachtet dat canRejectOrReturn útfiert, sil syn eigen mislearring resultearje yn de útfiering fan it fangenblok. As gefolch, de funksje foo sil einigje mei of ûndefiniearre (as neat wurdt weromjûn yn de try blok) of mei in flater fongen. As gefolch, dizze funksje sil net mislearje omdat de try/catch sil omgean de funksje foo sels.

Hjir is in oar foarbyld:

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

It is it wurdich omtinken te jaan oan it feit dat yn it foarbyld canRejectOrReturn wurdt weromjûn fan foo. Foo yn dit gefal einiget of mei in perfekte nûmer of jout in flater ("Sorry, nûmer te grut"). It fangen blok sil nea wurde útfierd.

It probleem is dat foo de belofte weromkomt dy't troch canRejectOrReturn trochjûn is. Dus de oplossing foar foo wurdt de oplossing foar canRejectOrReturn. Yn dit gefal sil de koade bestean út mar twa rigels:

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

Hjir is wat der bart as jo await brûke en tegearre weromkomme:

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

Yn 'e koade hjirboppe sil foo mei súkses útgean mei sawol in perfekt nûmer as in flater fongen. D'r sille hjir gjin wegeringen wêze. Mar foo sil weromkomme mei canRejectOrReturn, net mei undefined. Lit ús der wis fan meitsje troch it fuortheljen fan de return await canRejectOrReturn() line:

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

Algemiene flaters en falkûlen

Yn guon gefallen kin it brûken fan Async / Wachtsje liede ta flaters.

Fergetten wachtsje

Dit bart frij faak - it wachtsjen kaaiwurd is fergetten foar de belofte:

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

Sa't jo sjen kinne, is d'r gjin wachtsjen of werom yn 'e koade. Dêrom foo altyd útgiet mei undefined sûnder in 1 sekonde fertraging. Mar de belofte sil folbrocht wurde. As it in flater of ôfwizing smyt, dan sil UnhandledPromiseRejectionWarning wurde neamd.

Async-funksjes yn callbacks

Async-funksjes wurde frij faak brûkt yn .map of .filter as callbacks. In foarbyld is de fetchPublicReposCount(brûkersnamme) funksje, dy't it oantal iepen repositories op GitHub weromjout. Litte wy sizze dat d'r trije brûkers binne waans metriken wy nedich binne. Hjir is de koade foar dizze taak:

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

Wy moatte ArfatSalman, octocat, norvig akkounts. Yn dit gefal dogge wy:

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

It is de muoite wurdich omtinken te jaan oan Await yn 'e .map callback. Hjir telt is in rige fan beloften, en .map is in anonime callback foar eltse oantsjutte brûker.

Te konsekwint gebrûk fan wachtsje

Litte wy dizze koade as foarbyld nimme:

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

Hjir wurdt it repo-nûmer yn 'e count fariabele pleatst, dan wurdt dit nûmer tafoege oan 'e counts array. It probleem mei de koade is dat oant de gegevens fan 'e earste brûker fan' e server komme, sille alle folgjende brûkers yn 'e standby-modus wêze. Sa wurdt mar ien brûker tagelyk ferwurke.

As it bygelyks sa'n 300 ms duorret om ien brûker te ferwurkjen, dan is it foar alle brûkers al in twadde ôfhinklik fan it oantal brûkers. Mar om't it krijen fan it oantal repo net fan elkoar ôfhinget, kinne de prosessen parallelisearre wurde. Dit fereasket wurkjen mei .map en 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 ûntfangt in array fan beloften as ynfier en jout in belofte werom. Dat lêste is foltôge nei't alle beloften yn 'e array foltôge binne of by de earste ôfwizing. It kin barre dat se net allegear tagelyk begjinne - om simultane start te garandearjen, kinne jo p-map brûke.

konklúzje

Async-funksjes wurde hieltyd wichtiger foar ûntwikkeling. No, foar adaptyf gebrûk fan asyngronisaasjefunksjes moatte jo brûke Async iterators. In JavaSkript-ûntwikkelder soe hjir goed yn moatte wêze.

Skillbox advisearret:

Boarne: www.habr.com

Add a comment