Tarkastellaan Async/Awaitia JavaScriptissä esimerkkien avulla

Artikkelin kirjoittaja tutkii esimerkkejä Async/Awaitista JavaScriptissä. Kaiken kaikkiaan Async/Await on kätevä tapa kirjoittaa asynkronista koodia. Ennen tämän ominaisuuden ilmestymistä tällainen koodi kirjoitettiin takaisinsoittojen ja lupausten avulla. Alkuperäisen artikkelin kirjoittaja paljastaa Async/Awaitin edut analysoimalla erilaisia ​​esimerkkejä.

Muistutamme sinua: kaikille "Habrin" lukijoille - 10 000 ruplan alennus ilmoittautuessaan mille tahansa Skillbox-kurssille "Habr" -tarjouskoodilla.

Skillbox suosittelee: Kouluttava verkkokurssi "Java-kehittäjä".

Takaisinsoitto

Takaisinsoitto on toiminto, jonka puhelu viivästyy määräämättömän ajan. Aikaisemmin takaisinkutsuja käytettiin niillä koodialueilla, joissa tulosta ei saatu heti.

Tässä on esimerkki Node.js-tiedoston asynkronisesta lukemisesta:

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

Ongelmia syntyy, kun sinun on suoritettava useita asynkronisia toimintoja kerralla. Kuvitellaanpa tämä skenaario: Arfat-käyttäjätietokantaan tehdään pyyntö, sinun on luettava sen profile_img_url-kenttä ja ladattava kuva someserver.com-palvelimelta.
Lataamisen jälkeen muunnamme kuvan toiseen muotoon, esimerkiksi PNG:stä JPEG:ksi. Jos muunnos onnistui, käyttäjän sähköpostiin lähetetään kirje. Seuraavaksi tiedot tapahtumasta syötetään transformations.log-tiedostoon, jossa ilmoitetaan päivämäärä.

Kannattaa kiinnittää huomiota takaisinsoittojen päällekkäisyyteen ja suuriin }) -määriin koodin viimeisessä osassa. Sitä kutsutaan Callback Hell tai Pyramid of Doom.

Tämän menetelmän haitat ovat ilmeiset:

  • Tätä koodia on vaikea lukea.
  • Virheiden käsittely on myös vaikeaa, mikä usein johtaa huonoon koodin laatuun.

Tämän ongelman ratkaisemiseksi JavaScriptiin lisättiin lupauksia. Niiden avulla voit korvata takaisinsoittojen syvän sisäkkäisyyden sanalla .hen.

Lupausten myönteinen puoli on, että ne tekevät koodista paljon paremmin luettavissa, ylhäältä alas eikä vasemmalta oikealle. Lupauksilla on kuitenkin myös omat ongelmansa:

  • Sinun on lisättävä paljon .sitten.
  • Try/catch-komennolla .catchia käytetään käsittelemään kaikki virheet.
  • Työskentely useiden lupausten kanssa yhden silmukan sisällä ei ole aina kätevää; joissakin tapauksissa ne monimutkaistavat koodia.

Tässä on ongelma, joka näyttää viimeisen kohdan merkityksen.

Oletetaan, että meillä on for-silmukka, joka tulostaa numerosarjan 0–10 satunnaisin väliajoin (0–n sekuntia). Lupauksia käyttämällä sinun on muutettava tätä silmukkaa niin, että numerot tulostetaan järjestyksessä 0:sta 10:een. Joten jos nollan tulostaminen kestää 6 sekuntia ja ykkösen tulostaminen 2 sekuntia, nolla tulee tulostaa ensin ja sitten lähtölaskenta aloitetaan tulostamista varten.

Ja tietenkään emme käytä Async/Await- tai .sort-tiedostoa tämän ongelman ratkaisemiseen. Lopussa on esimerkkiratkaisu.

Asynkronointitoiminnot

Async-toimintojen lisääminen ES2017:ään (ES8) yksinkertaisti lupausten kanssa työskentelyä. Huomaan, että async-funktiot toimivat "lupausten päällä". Nämä funktiot eivät edusta laadullisesti erilaisia ​​käsitteitä. Async-funktiot on tarkoitettu vaihtoehdoksi lupauksia käyttävälle koodille.

Async/Await mahdollistaa työn järjestämisen asynkronisella koodilla synkroniseen tyyliin.

Siten lupausten tietäminen helpottaa Async/Awaitin periaatteiden ymmärtämistä.

syntaksi

Normaalisti se koostuu kahdesta avainsanasta: async ja await. Ensimmäinen sana muuttaa funktion asynkroniseksi. Tällaiset toiminnot mahdollistavat odotustoiminnon käytön. Kaikissa muissa tapauksissa tämän toiminnon käyttäminen aiheuttaa virheen.

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

Async lisätään aivan funktion määrityksen alkuun ja nuolifunktion tapauksessa =-merkin ja sulkeiden väliin.

Nämä funktiot voidaan sijoittaa objektiin menetelminä tai käyttää luokkamäärityksessä.

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

HUOM! On syytä muistaa, että luokan rakentajat ja getterit/setterit eivät voi olla asynkronisia.

Semantiikka ja suoritussäännöt

Async-funktiot ovat periaatteessa samanlaisia ​​kuin tavalliset JS-funktiot, mutta poikkeuksiakin on.

Siten async-funktiot palauttavat aina lupaukset:

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

Tarkemmin sanottuna fn palauttaa merkkijonon hello. No, koska tämä on asynkroninen funktio, merkkijonoarvo kääritään lupaukseen käyttämällä konstruktoria.

Tässä on vaihtoehtoinen muotoilu ilman Asyncia:

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

Tässä tapauksessa lupaus palautetaan "manuaalisesti". Asynkroninen toiminto on aina kääritty uuteen lupaukseen.

Jos palautusarvo on primitiivinen, async-funktio palauttaa arvon käärimällä sen lupaukseen. Jos palautusarvo on lupausobjekti, sen resoluutio palautetaan uudessa lupauksessa.

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

Mutta mitä tapahtuu, jos asynkronisessa funktiossa on virhe?

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

Jos sitä ei käsitellä, foo() palauttaa lupauksen hylättynä. Tässä tilanteessa virheen sisältävä Promise.reject palautetaan Promise.resolven sijaan.

Async-funktiot tulostavat aina lupauksen riippumatta siitä, mitä palautetaan.

Asynkroniset toiminnot pysähtyvät jokaisen odotuksen yhteydessä.

Odota vaikuttaa ilmaisuihin. Joten jos lauseke on lupaus, async-toiminto keskeytetään, kunnes lupaus on täytetty. Jos lauseke ei ole lupaus, se muunnetaan lupaukseksi Promise.resolven kautta ja täydennetään sitten.

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

Ja tässä on kuvaus fn-funktion toiminnasta.

  • Sen kutsumisen jälkeen ensimmäinen rivi muunnetaan arvosta const a = await 9; in const a = odota Promise.resolve(9);.
  • Awaitin käytön jälkeen funktion suoritus keskeytetään, kunnes a saa arvon (nykyisessä tilanteessa se on 9).
  • delayAndGetRandom(1000) keskeyttää fn-funktion suorittamisen, kunnes se suorittaa itsensä loppuun (1 sekunnin kuluttua). Tämä pysäyttää fn-toiminnon tehokkaasti 1 sekunniksi.
  • delayAndGetRandom(1000) ratkaisee palauttaa satunnaisarvon, joka sitten määritetään muuttujalle b.
  • No, tapaus muuttujan c kanssa on samanlainen kuin muuttujan a tapaus. Sen jälkeen kaikki pysähtyy hetkeksi, mutta nyt delayAndGetRandom(1000) ei palauta mitään, koska sitä ei vaadita.
  • Tämän seurauksena arvot lasketaan kaavalla a + b * c. Tulos kääritään lupaukseen käyttämällä Promise.resolvea ja funktio palauttaa sen.

Nämä tauot saattavat muistuttaa ES6:n generaattoreita, mutta siinä on jotain perää sinun syysi.

Ongelman ratkaiseminen

No, katsotaan nyt ratkaisua yllä mainittuun ongelmaan.

FinishMyTask-funktio käyttää Await-toimintoa odottaakseen toimintojen, kuten queryDatabase, sendEmail, logTaskInFile ja muiden, tuloksia. Jos vertaat tätä ratkaisua siihen, jossa lupauksia käytettiin, yhtäläisyydet tulevat ilmeisiksi. Async/Await-versio yksinkertaistaa kuitenkin suuresti kaikkia syntaktisia monimutkaisia ​​asioita. Tässä tapauksessa ei ole paljon takaisinsoittoja ja ketjuja, kuten .then/.catch.

Tässä on ratkaisu numeroiden ulostulolla, vaihtoehtoja on kaksi.

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

Ja tässä on ratkaisu async-funktioilla.

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

Käsittelyvirhe

Käsittelemättömät virheet on kääritty hylättyyn lupaukseen. Asynkronointifunktiot voivat kuitenkin käyttää try/catch-toimintoa käsitelläkseen virheitä synkronisesti.

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() on asynkroninen funktio, joka joko onnistuu ("täydellinen luku") tai epäonnistuu virheellä ("Anteeksi, numero on liian suuri").

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

Koska yllä olevassa esimerkissä oletetaan canRejectOrReturn-toiminnon suorittavan, sen oma epäonnistuminen johtaa catch-lohkon suorittamiseen. Tämän seurauksena funktio foo päättyy joko määrittelemättömään (kun try-lohkossa ei palauteta mitään) tai havaittuun virheeseen. Tämän seurauksena tämä toiminto ei epäonnistu, koska try/catch käsittelee itse funktion foo.

Tässä on toinen esimerkki:

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

Kannattaa kiinnittää huomiota siihen, että esimerkissä canRejectOrReturn palautetaan foo:sta. Foo tässä tapauksessa joko päättyy täydelliseen numeroon tai palauttaa virheilmoituksen ("Anteeksi, numero on liian suuri"). Catch-lohkoa ei koskaan suoriteta.

Ongelmana on, että foo palauttaa canRejectOrReturnilta antaman lupauksen. Joten ratkaisu foo tulee ratkaisuksi canRejectOrReturn. Tässä tapauksessa koodi koostuu vain kahdesta rivistä:

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

Näin tapahtuu, jos käytät odota ja palaat yhdessä:

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

Yllä olevassa koodissa foo poistuu onnistuneesti sekä täydellisellä numerolla että virheellä. Täällä ei tule kieltäytymään. Mutta foo palaa canRejectOrReturnilla, ei undefinedilla. Varmistetaan tämä poistamalla rivin return await canRejectOrReturn()

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

Yleisiä virheitä ja sudenkuoppia

Joissakin tapauksissa Async/Awaitin käyttö voi johtaa virheisiin.

Unohdettu odottaa

Tätä tapahtuu melko usein - odotusavainsana unohtuu ennen lupausta:

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

Kuten näet, koodissa ei ole odotusta tai paluuta. Siksi foo poistuu aina undefinedilla ilman 1 sekunnin viivettä. Mutta lupaus toteutuu. Jos se antaa virheen tai hylkäämisen, UnhandledPromiseRejectionWarning kutsutaan.

Asynkronointitoiminnot takaisinkutsuissa

Async-funktioita käytetään melko usein .map- tai .filter-tiedostoissa takaisinkutsuina. Esimerkki on fetchPublicReposCount(username) -funktio, joka palauttaa GitHubissa olevien avoimien arkiston määrän. Oletetaan, että tarvitsemme kolme käyttäjää. Tässä on koodi tälle tehtävälle:

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

Tarvitsemme ArfatSalman, octocat, norvig tilejä. Tässä tapauksessa teemme:

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

Kannattaa kiinnittää huomiota Odota-kohtaan .map-soittopyynnössä. Tässä on joukko lupauksia, ja .map on anonyymi takaisinsoitto jokaiselle määritetylle käyttäjälle.

Odotuksen liian johdonmukainen käyttö

Otetaan tämä koodi esimerkkinä:

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

Tässä repo-numero sijoitetaan count-muuttujaan, sitten tämä numero lisätään counts-taulukkoon. Koodin ongelmana on, että siihen asti, kunnes ensimmäisen käyttäjän tiedot saapuvat palvelimelta, kaikki seuraavat käyttäjät ovat valmiustilassa. Näin ollen vain yksi käyttäjä käsitellään kerrallaan.

Jos esimerkiksi yhden käyttäjän käsittelyyn kuluu noin 300 ms, niin kaikilla käyttäjillä se on jo toinen, kuluva aika riippuu lineaarisesti käyttäjien määrästä. Mutta koska repo-määrän saaminen ei riipu toisistaan, prosessit voidaan rinnastaa. Tämä edellyttää työskentelyä .map- ja Promise.all-tiedostojen kanssa:

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

Promise.all vastaanottaa joukon lupauksia syötteenä ja palauttaa lupauksen. Jälkimmäinen, kun kaikki taulukon lupaukset ovat täyttyneet tai ensimmäisellä hylkäämisellä, on suoritettu. Saattaa käydä niin, että ne eivät kaikki käynnisty samaan aikaan - samanaikaisen käynnistyksen varmistamiseksi voit käyttää p-karttaa.

Johtopäätös

Async-toiminnot ovat yhä tärkeämpiä kehityksen kannalta. No, async-toimintojen mukautuvaan käyttöön kannattaa käyttää Async Iteraattorit. JavaScript-kehittäjän tulisi olla perehtynyt tähän hyvin.

Skillbox suosittelee:

Lähde: will.com

Lisää kommentti