Utforsking av Async/Await i JavaScript gjennom eksempler

Artikkelforfatteren utforsker Async/Await i JavaScript gjennom eksempler. Generelt er Async/Await en praktisk mÄte Ä skrive asynkron kode pÄ. FÞr denne muligheten ble slik kode skrevet med bruk av tilbakeringinger og lÞfter. Forfatteren av den originale artikkelen fremhever fordelene ved Async/Await ved Ä analysere forskjellige eksempler.

Vi minner om: for alle lesere av «Habr» — 10 000 rubler rabatt ved pĂ„melding til hvilket som helst Skillbox-kurs med kampanjekoden «Habr».

Skillbox anbefaler: Nettbasert utdanningskurs «Java-utvikler».

Tilbakeringing

En tilbakeringing er en funksjon som kalles pÄ et udefinert tidspunkt. Tidligere ble tilbakeringinger brukt i de delene av koden der resultatet ikke kunne oppnÄs umiddelbart.

Her er et eksempel pÄ asynkron fillesing i Node.js:

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

Problemer oppstÄr nÄr flere asynkrone operasjoner mÄ utfÞres samtidig. La oss forestille oss et scenario der en forespÞrsel gjÞres til databasen for brukeren Arfat, og vi mÄ hente hans felt profile_img_url og laste opp bildet fra serveren som omhandler someserver.com.
Etter opplastingen konverterer vi bildet til et annet format, for eksempel fra PNG til JPEG. Hvis konverteringen er vellykket, sendes det en e-post til brukeren. Videre blir informasjon om hendelsen registrert i filen transformations.log med datoen angitt.

Det er viktig Ă„ merke seg at bruken av tilbakeringinger og det store antallet }) i den siste delen av koden kan fĂžre til det vi kaller Callback Hell eller Pyramid of Doom.

Ulempene ved denne metoden er Äpenbare:

  • Denne koden er vanskelig Ă„ lese.
  • Det er ogsĂ„ vanskelig Ă„ hĂ„ndtere feil, noe som ofte fĂžrer til at kvaliteten pĂ„ koden reduseres.

For Ă„ lĂžse dette problemet ble lĂžfter introdusert i JavaScript. De gjĂžr det mulig Ă„ erstatte dypridning av tilbakeringinger med .then.

En positiv ting med lÞfter er at koden blir mye mer lesbar, og at man kan lese den ovenfra og ned, heller enn fra venstre til hÞyre. Likevel har lÞfter ogsÄ sine egne problemer:

  • Det mĂ„ legges til mange .then.
  • I stedet for try/catch brukes .catch for Ă„ hĂ„ndtere alle feil.
  • Å jobbe med flere lĂžfter innen en enkelt syklus er langt fra alltid praktisk, i noen tilfeller kompliserer de koden.

Her er en oppgave som vil illustrere betydningen av det siste punktet.

La oss anta at det finnes en for-lĂžkke som skriver ut en sekvens av tall fra 0 til 10 med tilfeldige intervaller (0–n sekunder). Ved Ă„ bruke lĂžfter mĂ„ vi endre denne lĂžkken slik at tallene skrives ut i rekkefĂžlge fra 0 til 10. SĂ„ hvis utskrift av null tar 6 sekunder og av en tar 2 sekunder, mĂ„ null skrives ut fĂžrst, fĂžr utskriften av en begynner.

Og selvfÞlgelig bruker vi ikke Async/Await eller .sort for Ä lÞse dette problemet. Eksempelet pÄ lÞsningen finner du pÄ slutten.

Async-funksjoner

InnfĂžringen av async-funksjoner i ES2017 (ES8) har gjort det enklere Ă„ arbeide med lĂžfter. Jeg vil merke at async-funksjoner fungerer "over" lĂžfter. Disse funksjonene representerer ikke kvalitativt forskjellige konsepter. Async-funksjoner ble tenkt som et alternativ til kode som bruker lĂžfter.

Async/Await gir mulighet til Ă„ organisere arbeidet med asynkron kode i synkron stil.

Dermed gjÞr kunnskap om lÞfter det lettere Ä forstÄ prinsippene bak Async/Await.

Syntaks

I normale situasjoner bestÄr det av to spesifikke ord: async og await. Det fÞrste ordet gjÞr funksjonen asynkron. I slike funksjoner er bruken av await tillatt. I alle andre tilfeller vil bruk av dette ordet fÞre til 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 i begynnelsen av funksjonsdeklarasjonen, og i tilfelle et pilfunksjonsbruk—mellom likhetstegnet «=» og parentesene.

Disse funksjonene kan plasseres i et objekt som metoder eller brukes i klasseerklĂŠringen.

// 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 viktig Ă„ huske at klassekonstruktĂžrer og gettere/settere ikke kan vĂŠre asynkrone.

Semantikk og utfĂžrelsesregler

Async-funksjoner ligner i prinsippet pÄ standard JS-funksjoner, men det finnes unntak.

SĂ„, async-funksjoner returnerer alltid lĂžfter:

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

Specielt returnerer fn strengen hei. Siden dette er en asynkron funksjon, pakkes verdien av strengen inn i et lĂžfte ved hjelp av konstruktĂžren.

Her er en alternativ konstruksjon uten Async:

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

I dette tilfellet skjer retur av lĂžftet "manuelt". En asynkron funksjon pakkes alltid inn i et nytt lĂžfte.

NÄr returverdien er en primitiv, returnerer async-funksjonen verdien innpakket i et lÞfte. NÄr returverdien faktisk er et lÞfteobjekt, returneres opplÞsningen i et nytt lÞfte.

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

Promise.resolve(p) === p;
// true
 

Men hva skjer dersom det oppstÄr en feil inne i den asynkrone funksjonen?

async function foo() {
  throw Error('bar');
}

foo().catch(console.log);

Dersom den ikke hÄndteres, vil foo() returnere et lÞfte med avvisning. I denne situasjonen vil Promise.reject bli returnert i stedet for Promise.resolve, og inneholde feilen.

Asynkrone funksjoner gir alltid et lĂžfte ut, uansett hva som returneres.

Asynkrone funksjoner pauser ved hvert await.

Await pÄvirker uttrykk. SÄ, hvis uttrykket er et lÞfte, vil async-funksjonen pauses til lÞftet er fullfÞrt. Dersom uttrykket ikke er et lÞfte, konverteres det til et lÞfte gjennom Promise.resolve, og fullfÞres deretter.

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

Her er en beskrivelse av hvordan fn-funksjonen fungerer.

  • Etter at den er kalt, konverteres den fĂžrste linjen fra const a = await 9; til const a = await Promise.resolve(9);.
  • Etter Ă„ ha brukt Await pauses utfĂžrelsen av funksjonen til a fĂ„r sin verdi (i dette tilfellet 9).
  • delayAndGetRandom(1000) pauser utfĂžrelsen av fn-funksjonen til det er fullfĂžrt (etter 1 sekund). Dette er faktisk en stopp av fn-funksjonen i 1 sekund.
  • delayAndGetRandom(1000) returnerer et tilfeldig verdi gjennom resolve, som deretter tildeles variabelen b.
  • Situasjonen med variabel c er lik tilfelle med variabel a. Deretter stopper alt i ett sekund, men nĂ„ returnerer ikke delayAndGetRandom(1000) noe, da det ikke er nĂždvendig.
  • Til slutt beregnes verdiene etter formelen a + b * c. Resultatet pakkes inn i et lĂžfte ved hjelp av Promise.resolve og returneres fra funksjonen.

Disse pausene kan minne om generatorer i ES6, men det finnes sine grunner.

LĂžser oppgaven

La oss nÄ se pÄ lÞsningen pÄ oppgaven som ble nevnt tidligere.

I funksjonen finishMyTask bruker vi Await for Ä vente pÄ resultater fra operasjoner som queryDatabase, sendEmail, logTaskInFile og flere andre. NÄr vi sammenligner denne tilnÊrmingen med den som brukte lÞfter, blir likheten Äpenbar. Likevel forenkler Async/Await-versjonen betydelig syntaksen. I dette tilfellet er det ikke mange tilbakeringinger og kjeder som .then/.catch.

Her er en lĂžsning med tallutskrift, som har 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);
    });
  });
};

Her er en lĂžsning som bruker async-funksjoner.

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

FeilhÄndtering

Ubehandlede feil pakkes inn i et avvist lÞfte. Likevel kan vi i async-funksjoner bruke try/catch-strukturen for Ä hÄndtere feil synkront.

async function canRejectOrReturn() {
  // vent ett sekund
  await new Promise(res => setTimeout(res, 1000));
 
  // Avvis med ~50% sannsynlighet
  if (Math.random() > 0.5) {
    throw new Error('Beklager, tallet er for stort.')
  }
 
  return 'perfekt tall';
}

canRejectOrReturn() er en asynkron funksjon som enten fullfþrer vellykket (“perfekt tall”), eller mislykkes med en feil (“Beklager, tallet er for stort”).

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'feil fanget';
  }
}

Siden det i eksemplet ovenfor ventes at canRejectOrReturn utfÞres, vil en egen mislykket avslutning fÞre til at catch-blokken utfÞres. Dermed vil foo-funksjonen avsluttes med enten undefined (nÄr ingenting returneres i try-blokken), eller med feil fanget. Resultatet er at denne funksjonen ikke vil mislykkes, siden try/catch tar seg av feilene fra foo-funksjonen selv.

Her er et annet eksempel:

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

Det er viktig Ă„ merke seg at i eksempelet fra foo returneres canRejectOrReturn. Foo vil i dette tilfellet avsluttes med enten perfekt tall eller en feil (Error) (“Beklager, tallet er for stort”). Catch-blokken vil aldri bli utfĂžrt.

Problemet er at foo returnerer et lÞfte, som videreformidles fra canRejectOrReturn. SÄ lÞsningen i foo blir lÞsningen for canRejectOrReturn. I dette tilfellet vil koden bestÄ av bare to linjer:

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

Her er hva som skjer hvis vi bruker bÄde await og return sammen:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'feil fanget';
  }
}

I koden ovenfor vil foo avsluttes vellykket med bÄde perfekt tall og feil fanget. Her vil det ikke vÊre noen avvisninger. Men foo vil avsluttes med canRejectOrReturn, og ikke med undefined. La oss sjekke dette ved Ä fjerne linjen return await canRejectOrReturn():

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


Vanlige feil og fallgruver

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

Glemt await

Dette skjer ganske ofte — man glemmer nþkkelordet await fþr en promise:

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

I koden er det, som man kan se, verken await eller return. Dermed avsluttes foo alltid med undefined uten en forsinkelse pÄ 1 sekund. Men promisen vil bli utfÞrt. Hvis den gir en feil eller blir avvist, vil UnhandledPromiseRejectionWarning bli kalt.

Async-funksjoner i tilbakeringinger

Async-funksjoner brukes ganske ofte i .map eller .filter som tilbakeringinger. Et eksempel kan vÊre funksjonen fetchPublicReposCount(username), som returnerer antallet Äpne GitHub-repositorier. La oss si at vi har tre brukere hvis statistikk 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 kontoene ArfatSalman, octocat, norvig. I sÄ fall kjÞrer vi:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];

const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

Det er verdt Ă„ merke seg await i tilbakeringingsfunksjonen .map. Her er counts en array av promises, og .map er en anonym tilbakeringing for hver angitt bruker.

Overdreven sekvensiell bruk av await

La oss ta en slik kode som 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 antallet repoer i variabelen count, deretter legges dette tallet til i arrayen counts. Problemet med koden er at mens dataene fra serveren for den fÞrste brukeren kommer, vil alle fÞlgende brukere vÊre i ventemodus. Dermed kan bare én bruker behandles om gangen.

Hvis behandling av én bruker tar omtrent 300 ms, vil det for alle brukere vÊre én sekund; den totale tiden avhenger lineÊrt av antall brukere. Men siden innhentingen av antall repoer ikke avhenger av hverandre, kan prosessene parallelliseres. For dette trenger vi Ä jobbe 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 tar inn en matrise av lĂžfter som returnerer et lĂžfte. Det fullfĂžres enten nĂ„r alle lĂžfter i matrisen er fullfĂžrt eller ved fĂžrste avvisning. Det kan skje at de ikke startes samtidig, — for Ă„ sikre samtidig start kan man bruke p-map.

Konklusjon

Async-funksjoner blir stadig viktigere for utvikling. For tilpasset bruk av async-funksjoner kan det vÊre lurt Ä bruke Async Iterators. En JavaScript-utvikler mÄ forstÄ dette godt.

Skillbox anbefaler:

Kilde: habr.com

KjĂžp pĂ„litelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere đŸ”„ KjĂžp pĂ„litelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster