Pažvelkime į „Async/Await“ „JavaScript“ naudodami pavyzdžius

Straipsnio autorius nagrinėja „Async/Await“ pavyzdžius „JavaScript“. Apskritai „Async/Await“ yra patogus būdas rašyti asinchroninį kodą. Prieš atsirandant šiai funkcijai, toks kodas buvo parašytas naudojant atgalinius skambučius ir pažadus. Originalaus straipsnio autorius, analizuodamas įvairius pavyzdžius, atskleidžia Async/Await privalumus.

Primename: visiems „Habr“ skaitytojams – 10 000 rublių nuolaida užsiregistravus į bet kurį „Skillbox“ kursą naudojant „Habr“ reklamos kodą.

„Skillbox“ rekomenduoja: Mokomasis internetinis kursas „Java kūrėjas“.

Callback

Atgalinis skambutis yra funkcija, kurios skambutis atidėtas neribotam laikui. Anksčiau atgaliniai skambučiai buvo naudojami tose kodo srityse, kuriose rezultato nebuvo galima gauti iš karto.

Štai pavyzdys, kaip asinchroniškai nuskaityti failą Node.js:

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

Problemos kyla, kai vienu metu reikia atlikti kelias asinchronines operacijas. Įsivaizduokime tokį scenarijų: pateikiama užklausa Arfat vartotojų duomenų bazei, reikia perskaityti jos laukelį profile_img_url ir atsisiųsti paveikslėlį iš someserver.com serverio.
Atsisiuntę vaizdą konvertuojame į kitą formatą, pavyzdžiui, iš PNG į JPEG. Jei konversija buvo sėkminga, vartotojo el. paštu išsiunčiamas laiškas. Toliau informacija apie įvykį įvedama į transformations.log failą, nurodant datą.

Verta atkreipti dėmesį į atgalinių skambučių sutapimą ir didelį skaičių }) paskutinėje kodo dalyje. Tai vadinama Atšaukimo pragaru arba Pražūties piramide.

Šio metodo trūkumai yra akivaizdūs:

  • Šį kodą sunku perskaityti.
  • Taip pat sunku tvarkyti klaidas, o tai dažnai lemia prastą kodo kokybę.

Norėdami išspręsti šią problemą, „JavaScript“ buvo pridėta pažadų. Jie leidžia pakeisti gilų atgalinių skambučių įdėjimą žodžiu .the.

Teigiamas pažadų aspektas yra tas, kad dėl jų kodas yra daug geriau skaitomas iš viršaus į apačią, o ne iš kairės į dešinę. Tačiau pažadai taip pat turi savo problemų:

  • Reikia pridėti daug .tada.
  • Vietoj try/catch, visoms klaidoms tvarkyti naudojamas .catch.
  • Dirbti su keliais pažadais vienoje kilpoje ne visada patogu; kai kuriais atvejais jie apsunkina kodą.

Čia yra problema, kuri parodys paskutinio punkto prasmę.

Tarkime, kad turime for kilpą, kuri atsitiktiniais intervalais (0–n sekundžių) spausdina skaičių seką nuo 10 iki 0. Naudojant pažadus, reikia pakeisti šią kilpą taip, kad skaičiai būtų spausdinami iš eilės nuo 0 iki 10. Taigi, jei nulis atspausdinamas per 6 sekundes, o vienetas – per 2 sekundes, pirmiausia reikia atspausdinti nulį, o tada prasidės spausdinimo atgalinis skaičiavimas.

Ir, žinoma, šiai problemai išspręsti nenaudojame Async/Await arba .sort. Sprendimo pavyzdys yra pabaigoje.

Asinchronizavimo funkcijos

Asinchroninių funkcijų pridėjimas ES2017 (ES8) supaprastino darbo su pažadais užduotį. Pastebiu, kad asinchroninės funkcijos veikia „viršuje“ pažadų. Šios funkcijos neatspindi kokybiškai skirtingų sąvokų. Asinchronizavimo funkcijos skirtos kaip alternatyva kodui, kuris naudoja pažadus.

Async/Await leidžia organizuoti darbą su asinchroniniu kodu sinchroniniu stiliumi.

Taigi, žinant pažadus lengviau suprasti Async/Await principus.

sintaksė

Paprastai jį sudaro du raktiniai žodžiai: async ir await. Pirmasis žodis paverčia funkciją asinchronine. Tokios funkcijos leidžia naudoti laukimą. Bet kuriuo kitu atveju naudojant šią funkciją bus sukurta klaida.

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

Async įterpiamas pačioje funkcijos deklaracijos pradžioje, o rodyklės funkcijos atveju – tarp „=“ ženklo ir skliaustų.

Šios funkcijos gali būti dedamos į objektą kaip metodus arba naudojamos klasės deklaracijoje.

// 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! Verta prisiminti, kad klasių konstruktoriai ir geteriai/seteriai negali būti asinchroniški.

Semantika ir vykdymo taisyklės

Asinchroninės funkcijos iš esmės yra panašios į standartines JS funkcijas, tačiau yra išimčių.

Taigi asinchroninės funkcijos visada grąžina pažadus:

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

Tiksliau, fn grąžina eilutę labas. Na, kadangi tai yra asinchroninė funkcija, eilutės reikšmė suvyniojama į pažadą naudojant konstruktorių.

Štai alternatyvus dizainas be async:

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

Tokiu atveju pažadas grąžinamas „rankiniu būdu“. Asinchroninė funkcija visada apgaubta nauju pažadu.

Jei grąžinama vertė yra primityvi, asinchronizavimo funkcija grąžina vertę, suvyniodama ją į pažadą. Jei grąžinama vertė yra pažado objektas, jo rezoliucija grąžinama nauju pažadu.

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

Bet kas atsitiks, jei asinchroninėje funkcijoje yra klaida?

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

Jei jis nebus apdorotas, foo() grąžins pažadą su atmetimu. Esant tokiai situacijai, vietoj Promise.resolve bus grąžintas Promise.reject, kuriame yra klaida.

Asinchroninės funkcijos visada pateikia pažadą, neatsižvelgiant į tai, kas grąžinama.

Asinchroninės funkcijos pristabdo kiekvieną laukimą.

Laukti įtakos išraiškos. Taigi, jei išraiška yra pažadas, asinchronizavimo funkcija sustabdoma, kol pažadas bus įvykdytas. Jei išraiška nėra pažadas, ji paverčiama pažadu per Promise.resolve ir baigiama.

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

Ir čia yra aprašymas, kaip veikia fn funkcija.

  • Ją iškvietus pirmoji eilutė konvertuojama iš const a = laukti 9; in const a = laukti pažado.resolve(9);.
  • Panaudojus Await, funkcijos vykdymas sustabdomas, kol a įgaus reikšmę (dabartinėje situacijoje ji yra 9).
  • delayAndGetRandom(1000) pristabdo fn funkcijos vykdymą, kol ji pati baigsis (po 1 sekundės). Tai veiksmingai sustabdo fn funkciją 1 sekundei.
  • delayAndGetRandom(1000) per rezoliuciją grąžina atsitiktinę reikšmę, kuri tada priskiriama kintamajam b.
  • Na, atvejis su kintamuoju c yra panašus į atvejį su kintamuoju a. Po to viskas sekundei sustoja, bet dabar delayAndGetRandom(1000) nieko negrąžina, nes to nereikia.
  • Dėl to vertės apskaičiuojamos naudojant formulę a + b * c. Rezultatas suvyniotas į pažadą naudojant Promise.resolve ir grąžinamas funkcijos.

Šios pauzės gali priminti ES6 generatorius, tačiau jame yra kažkas jūsų priežastys.

Problemos sprendimas

Na, o dabar pažvelkime į aukščiau paminėtos problemos sprendimą.

Funkcija finishMyTask naudoja Await, kad lauktų operacijų rezultatų, tokių kaip queryDatabase, sendEmail, logTaskInFile ir kt. Jei palyginsite šį sprendimą su tuo, kuriame buvo naudojami pažadai, panašumai išryškės. Tačiau Async / Await versija labai supaprastina visus sintaksinius sudėtingumus. Šiuo atveju nėra daug atgalinių skambučių ir grandinių, tokių kaip .then/.catch.

Čia yra sprendimas su skaičių išvestimi, yra dvi galimybės.

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

Ir čia yra sprendimas naudojant asinchronines funkcijas.

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

Apdorojant įvyko klaida

Neapdorotos klaidos įvyniotos į atmestą pažadą. Tačiau asinchronizavimo funkcijos gali naudoti try/catch, kad sinchroniškai tvarkytų klaidas.

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() yra asinchroninė funkcija, kuri arba pavyksta („puikus skaičius“) arba nepavyksta dėl klaidos („Atsiprašome, skaičius per didelis“).

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

Kadangi aukščiau pateiktame pavyzdyje tikimasi, kad canRejectOrReturn bus vykdomas, dėl jo paties gedimo bus vykdomas gaudymo blokas. Dėl to funkcija foo baigsis arba neapibrėžta (kai nieko negrąžinama trynimo bloke) arba sugauta klaida. Dėl to ši funkcija nesuges, nes try/catch atliks pačią funkciją foo.

Štai dar vienas pavyzdys:

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

Verta atkreipti dėmesį į tai, kad pavyzdyje canRejectOrReturn grąžinamas iš foo. Foo šiuo atveju arba baigiasi tobulu skaičiumi, arba grąžina klaidą („Atsiprašome, skaičius per didelis“). Sugavimo blokas niekada nebus vykdomas.

Problema ta, kad „foo“ grąžina „canRejectOrReturn“ duotą pažadą. Taigi sprendimas foo tampa sprendimu canRejectOrReturn. Šiuo atveju kodą sudarys tik dvi eilutės:

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

Štai kas atsitiks, jei naudosite laukti ir grįšite kartu:

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

Aukščiau esančiame kode „foo“ sėkmingai išeis su puikiu numeriu ir sugauta klaida. Atsisakymų čia nebus. Bet foo grįš su canRejectOrReturn, o ne su undefined. Įsitikinkime tai pašalindami eilutę return await canRejectOrReturn():

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

Dažnos klaidos ir spąstai

Kai kuriais atvejais naudojant Async/Await gali atsirasti klaidų.

Pamirštas laukia

Taip nutinka gana dažnai – laukimo raktinis žodis pamirštamas prieš pažadą:

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

Kaip matote, kode nėra laukimo ar grįžimo. Todėl foo visada išeina su undefined be 1 sekundės uždelsimo. Bet pažadas bus įvykdytas. Jei bus rodoma klaida arba atmetimas, bus iškviestas UnhandledPromiseRejectionWarning.

Asinchroninės funkcijos per skambučius

Asinchroninės funkcijos gana dažnai naudojamos .map arba .filter kaip atgaliniai skambučiai. Pavyzdys yra funkcija fetchPublicReposCount(username), kuri grąžina atidarytų GitHub saugyklų skaičių. Tarkime, kad yra trys vartotojai, kurių metrikos mums reikia. Štai šios užduoties kodas:

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

Mums reikia ArfatSalman, octocat, norvig paskyrų. Šiuo atveju mes darome:

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

Verta atkreipti dėmesį į laukimą .map atgaliniame skambutyje. Čia skaičiai yra pažadų masyvas, o .map yra anoniminis kiekvieno nurodyto vartotojo skambutis.

Pernelyg nuoseklus laukimo naudojimas

Paimkime šį kodą kaip pavyzdį:

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

Čia atpirkimo numeris įdedamas į skaičiavimo kintamąjį, tada šis skaičius pridedamas prie skaičiavimų masyvo. Kodo problema yra ta, kad kol iš serverio nepateks pirmojo vartotojo duomenys, visi paskesni vartotojai veiks budėjimo režimu. Taigi vienu metu apdorojamas tik vienas vartotojas.

Jei, pavyzdžiui, vienam vartotojui apdoroti užtrunka apie 300 ms, tai visiems vartotojams tai jau antras laikas, sugaištas laikas tiesiškai priklauso nuo vartotojų skaičiaus. Tačiau kadangi atpirkimo sandorių skaičiaus gavimas nepriklauso vienas nuo kito, procesai gali būti lygiagretinami. Tam reikia dirbti su .map ir 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 kaip įvestį gauna daugybę pažadų ir grąžina pažadą. Pastaroji, įvykdžius visus pažadus masyve arba pirmą kartą atmetus, yra įvykdyta. Gali atsitikti taip, kad jie visi nepasileidžia vienu metu – norint užtikrinti vienalaikį startą, galite naudoti p-map.

išvada

Asinchroninės funkcijos tampa vis svarbesnės plėtrai. Na, norėdami adaptyviai naudoti asinchronines funkcijas, turėtumėte naudoti Asinchroniniai iteratoriai. „JavaScript“ kūrėjas turėtų tai gerai išmanyti.

„Skillbox“ rekomenduoja:

Šaltinis: www.habr.com

Добавить комментарий