La oss se på Async/Await i JavaScript ved å bruke eksempler

Forfatteren av artikkelen undersøker eksempler på Async/Await i JavaScript. Totalt sett er Async/Await en praktisk måte å skrive asynkron kode på. Før denne funksjonen dukket opp, ble slik kode skrevet ved hjelp av tilbakeringinger og løfter. Forfatteren av den originale artikkelen avslører fordelene med Async/Await ved å analysere ulike eksempler.

Vi minner om: for alle lesere av "Habr" - en rabatt på 10 000 rubler når du melder deg på et hvilket som helst Skillbox-kurs ved å bruke kampanjekoden "Habr".

Skillbox anbefaler: Pedagogisk nettkurs "Java-utvikler".

Ring Tilbake

Tilbakeringing er en funksjon hvis samtale er forsinket på ubestemt tid. Tidligere ble tilbakeringinger brukt i de kodeområdene hvor resultatet ikke kunne oppnås umiddelbart.

Her er et eksempel på asynkron lesing av en fil i Node.js:

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

Problemer oppstår når du må utføre flere asynkrone operasjoner samtidig. La oss forestille oss dette scenariet: en forespørsel sendes til Arfat-brukerdatabasen, du må lese profile_img_url-feltet og laste ned et bilde fra someserver.com-serveren.
Etter nedlasting konverterer vi bildet til et annet format, for eksempel fra PNG til JPEG. Hvis konverteringen var vellykket, sendes et brev til brukerens e-post. Deretter legges informasjon om hendelsen inn i transformations.log-filen, som indikerer datoen.

Det er verdt å ta hensyn til overlappingen av tilbakeringinger og det store antallet }) i den siste delen av koden. Det kalles Callback Hell eller Pyramid of Doom.

Ulempene med denne metoden er åpenbare:

  • Denne koden er vanskelig å lese.
  • Det er også vanskelig å håndtere feil, noe som ofte fører til dårlig kodekvalitet.

For å løse dette problemet ble løfter lagt til JavaScript. De lar deg erstatte dyp nesting av tilbakeringinger med ordet .then.

Det positive med løfter er at de gjør koden mye bedre lesbar, fra topp til bunn i stedet for fra venstre til høyre. Imidlertid har løfter også sine problemer:

  • Du må legge til mye .da.
  • I stedet for try/catch, brukes .catch for å håndtere alle feil.
  • Å jobbe med flere løfter innenfor én sløyfe er ikke alltid praktisk; i noen tilfeller kompliserer de koden.

Her er et problem som vil vise betydningen av det siste punktet.

Anta at vi har en for-løkke som skriver ut en tallsekvens fra 0 til 10 med tilfeldige intervaller (0–n sekunder). Ved å bruke løfter må du endre denne løkken slik at tallene skrives ut i rekkefølge fra 0 til 10. Så hvis det tar 6 sekunder å skrive ut en null og 2 sekunder å skrive ut en ener, skal nullen skrives ut først, og deretter nedtellingen for å skrive ut den vil begynne.

Og selvfølgelig bruker vi ikke Async/Await eller .sort for å løse dette problemet. Et eksempel på løsning er på slutten.

Asynkrone funksjoner

Tillegget av asynkrone funksjoner i ES2017 (ES8) forenklet oppgaven med å jobbe med løfter. Jeg legger merke til at asynkrone funksjoner fungerer "på toppen" av løftene. Disse funksjonene representerer ikke kvalitativt forskjellige konsepter. Asynkrone funksjoner er ment som et alternativ til kode som bruker løfter.

Async/Await gjør det mulig å organisere arbeid med asynkron kode i en synkron stil.

Å kjenne løfter gjør det dermed lettere å forstå prinsippene for Async/Await.

syntaks

Normalt består den av to nøkkelord: asynkron og avvent. Det første ordet gjør funksjonen til asynkron. Slike funksjoner tillater bruk av avvente. I alle andre tilfeller vil bruk av denne funksjonen generere en feil.

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

Async settes inn helt i begynnelsen av funksjonsdeklarasjonen, og i tilfelle av en pilfunksjon, mellom "="-tegnet og parentesene.

Disse funksjonene kan plasseres i et objekt som metoder eller brukes i en klasseerklæring.

// 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! Det er verdt å huske at klassekonstruktører og gettere/settere ikke kan være asynkrone.

Semantikk og utførelsesregler

Asynkrone funksjoner er i utgangspunktet lik standard JS-funksjoner, men det finnes unntak.

Dermed gir asynkrone funksjoner alltid løfter:

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

Nærmere bestemt returnerer fn strengen hei. Vel, siden dette er en asynkron funksjon, er strengverdien pakket inn i et løfte ved hjelp av en konstruktør.

Her er et alternativt design uten Async:

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

I dette tilfellet returneres løftet "manuelt". En asynkron funksjon er alltid pakket inn i et nytt løfte.

Hvis returverdien er en primitiv, returnerer asynkronfunksjonen verdien ved å pakke den inn i et løfte. Hvis returverdien er et løfteobjekt, returneres dets oppløsning i et nytt løfte.

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

Men hva skjer hvis det er en feil inne i en asynkron funksjon?

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

Hvis det ikke blir behandlet, vil foo() returnere et løfte med avvisning. I denne situasjonen vil Promise.reject som inneholder en feil returneres i stedet for Promise.resolve.

Asynkrone funksjoner gir alltid et løfte, uavhengig av hva som returneres.

Asynkrone funksjoner stopper hver gang du venter.

Avvent påvirker uttrykk. Så hvis uttrykket er et løfte, suspenderes asynkronfunksjonen til løftet er oppfylt. Hvis uttrykket ikke er et løfte, konverteres det til et løfte via Promise.resolve og deretter fullføres.

// 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 her er en beskrivelse av hvordan fn-funksjonen fungerer.

  • Etter å ha kalt den, blir den første linjen konvertert fra const a = await 9; in const a = await Promise.resolve(9);.
  • Etter bruk av Await suspenderes funksjonsutførelsen til a får sin verdi (i den nåværende situasjonen er den 9).
  • delayAndGetRandom(1000) stanser utførelsen av fn-funksjonen til den fullfører seg selv (etter 1 sekund). Dette stopper effektivt fn-funksjonen i 1 sekund.
  • delayAndGetRandom(1000) via resolve returnerer en tilfeldig verdi, som deretter tilordnes til variabelen b.
  • Vel, tilfellet med variabel c ligner tilfellet med variabel a. Etter det stopper alt et sekund, men nå returnerer delayAndGetRandom(1000) ingenting fordi det ikke er nødvendig.
  • Som et resultat beregnes verdiene ved å bruke formelen a + b * c. Resultatet er pakket inn i et løfte ved å bruke Promise.resolve og returneres av funksjonen.

Disse pausene kan minne om generatorer i ES6, men det er noe med det dine grunner.

Løser problemet

Vel, la oss nå se på løsningen på problemet nevnt ovenfor.

FinishMyTask-funksjonen bruker Await for å vente på resultatene av operasjoner som queryDatabase, sendEmail, logTaskInFile og andre. Hvis du sammenligner denne løsningen med den der løfter ble brukt, vil likhetene bli tydelige. Async/Await-versjonen forenkler imidlertid all syntaktisk kompleksitet. I dette tilfellet er det ikke et stort antall tilbakeringinger og kjeder som .then/.catch.

Her er en løsning med utdata av tall, det er to alternativer.

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 her er en løsning som bruker asynkrone funksjoner.

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

Feil under behandling

Uhåndterte feil er pakket inn i et avvist løfte. Asynkrone funksjoner kan imidlertid bruke try/catch for å håndtere feil synkront.

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 en asynkron funksjon som enten lykkes ("perfekt tall") eller mislykkes med en feil ("Beklager, tall for stort").

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

Siden eksemplet ovenfor forventer at canRejectOrReturn utføres, vil dets egen feil resultere i utførelse av catch-blokken. Som et resultat vil funksjonen foo ende med enten udefinert (når ingenting returneres i try-blokken) eller med en feil fanget. Som et resultat vil denne funksjonen ikke mislykkes fordi try/catch vil håndtere funksjonen foo selv.

Her er et annet eksempel:

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

Det er verdt å ta hensyn til det faktum at i eksemplet returneres canRejectOrReturn fra foo. Foo i dette tilfellet avsluttes enten med et perfekt tall eller returnerer en feil ("Beklager, nummeret er for stort"). Fangstblokken vil aldri bli utført.

Problemet er at foo returnerer løftet som ble gitt fra canRejectOrReturn. Så løsningen på foo blir løsningen på canRejectOrReturn. I dette tilfellet vil koden bare bestå av to linjer:

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

Her er hva som skjer hvis du bruker avvente og returnere sammen:

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

I koden ovenfor vil foo avslutte vellykket med både et perfekt tall og en feil fanget. Her blir det ingen avslag. Men foo kommer tilbake med canRejectOrReturn, ikke med undefined. La oss sørge for dette ved å fjerne return await canRejectOrReturn()-linjen:

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

Vanlige feil og fallgruver

I noen tilfeller kan bruk av Async/Await føre til feil.

Glemt venter

Dette skjer ganske ofte - vent-nøkkelordet er glemt før løftet:

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

Som du kan se, er det ingen venting eller retur i koden. Derfor går foo alltid ut med udefinert uten 1 sekunds forsinkelse. Men løftet vil bli oppfylt. Hvis det gir en feil eller avvisning, vil UnhandledPromiseRejectionWarning bli kalt.

Asynkrone funksjoner i tilbakeringinger

Asynkrone funksjoner brukes ganske ofte i .map eller .filter som tilbakeringinger. Et eksempel er funksjonen fetchPublicReposCount(brukernavn), som returnerer antall åpne depoter på GitHub. La oss si at det er tre brukere hvis beregninger vi trenger. Her er koden for denne oppgaven:

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 trenger ArfatSalman, octocat, norvig kontoer. I dette tilfellet gjør vi:

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

Det er verdt å ta hensyn til Await i .map-tilbakeringingen. Her teller er en rekke løfter, og .map er en anonym tilbakeringing for hver spesifisert bruker.

Altfor konsekvent bruk av avvente

La oss ta denne koden som et eksempel:

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

Her plasseres reponummeret i tellevariabelen, deretter legges dette tallet til tellematrisen. Problemet med koden er at inntil den første brukerens data kommer fra serveren, vil alle påfølgende brukere være i standby-modus. Dermed behandles kun én bruker om gangen.

Hvis det for eksempel tar ca. 300 ms å behandle én bruker, er det allerede et sekund for alle brukere; tiden brukt lineært avhenger av antall brukere. Men siden innhenting av antall repo ikke er avhengig av hverandre, kan prosessene parallelliseres. Dette krever arbeid med .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 mottar en rekke løfter som input og returnerer et løfte. Sistnevnte, etter at alle løftene i arrayet er fullført eller ved den første avvisningen, er fullført. Det kan skje at de ikke alle starter samtidig - for å sikre samtidig start kan du bruke p-map.

Konklusjon

Asynkrone funksjoner blir stadig viktigere for utvikling. Vel, for adaptiv bruk av asynkrone funksjoner, bør du bruke Asynkrone iteratorer. En JavaScript-utvikler bør være godt kjent med dette.

Skillbox anbefaler:

Kilde: www.habr.com

Legg til en kommentar