Lad os se på Async/Await i JavaScript ved hjælp af eksempler

Forfatteren af ​​artiklen undersøger eksempler på Async/Await i JavaScript. Samlet set er Async/Await en praktisk måde at skrive asynkron kode på. Før denne funktion dukkede op, blev en sådan kode skrevet ved hjælp af tilbagekald og løfter. Forfatteren til den originale artikel afslører fordelene ved Async/Await ved at analysere forskellige eksempler.

Påmindelse: for alle læsere af "Habr" - en rabat på 10 rubler ved tilmelding til ethvert Skillbox-kursus ved hjælp af "Habr"-kampagnekoden.

Skillbox anbefaler: Pædagogisk online kursus "Java-udvikler".

Tilbagekald

Tilbagekald er en funktion, hvis opkald er forsinket på ubestemt tid. Tidligere blev tilbagekald brugt i de kodeområder, hvor resultatet ikke umiddelbart kunne opnås.

Her er et eksempel på asynkron læsning af en fil i Node.js:

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

Problemer opstår, når du skal udføre flere asynkrone operationer på én gang. Lad os forestille os dette scenarie: Der sendes en anmodning til Arfat-brugerdatabasen, du skal læse dens profile_img_url-felt og downloade et billede fra someserver.com-serveren.
Efter download konverterer vi billedet til et andet format, for eksempel fra PNG til JPEG. Hvis konverteringen lykkedes, sendes et brev til brugerens e-mail. Dernæst indtastes oplysninger om hændelsen i transformations.log-filen, der angiver datoen.

Det er værd at være opmærksom på overlapningen af ​​tilbagekald og det store antal }) i den sidste del af koden. Det hedder Callback Hell eller Pyramid of Doom.

Ulemperne ved denne metode er indlysende:

  • Denne kode er svær at læse.
  • Det er også svært at håndtere fejl, hvilket ofte fører til dårlig kodekvalitet.

For at løse dette problem blev der tilføjet løfter til JavaScript. De giver dig mulighed for at erstatte dyb indlejring af tilbagekald med ordet .then.

Det positive aspekt ved løfter er, at de gør koden meget bedre læsbar, fra top til bund i stedet for fra venstre mod højre. Men løfter har også deres problemer:

  • Du skal tilføje en masse .så.
  • I stedet for try/catch, bruges .catch til at håndtere alle fejl.
  • Det er ikke altid praktisk at arbejde med flere løfter inden for en løkke; i nogle tilfælde komplicerer de koden.

Her er et problem, der vil vise betydningen af ​​det sidste punkt.

Antag, at vi har en for-løkke, der udskriver en række tal fra 0 til 10 med tilfældige intervaller (0–n sekunder). Ved at bruge løfter skal du ændre denne sløjfe, så tallene udskrives i rækkefølge fra 0 til 10. Så hvis det tager 6 sekunder at udskrive et nul og 2 sekunder at udskrive et et, skal nullet udskrives først, og derefter nedtællingen til udskrivning af den vil begynde.

Og selvfølgelig bruger vi ikke Async/Await eller .sort til at løse dette problem. Et eksempel på en løsning er til sidst.

Asynkrone funktioner

Tilføjelsen af ​​async-funktioner i ES2017 (ES8) forenklede opgaven med at arbejde med løfter. Jeg bemærker, at asynkrone funktioner fungerer "ovenpå" løfter. Disse funktioner repræsenterer ikke kvalitativt forskellige begreber. Asynkrone funktioner er tænkt som et alternativ til kode, der bruger løfter.

Async/Await gør det muligt at organisere arbejdet med asynkron kode i en synkron stil.

At kende løfter gør det således lettere at forstå principperne for Async/Await.

syntaks

Normalt består den af ​​to nøgleord: asynkron og afvent. Det første ord gør funktionen til asynkron. Sådanne funktioner tillader brugen af ​​afvent. I alle andre tilfælde vil brug af denne funktion generere en fejl.

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

Async indsættes helt i begyndelsen af ​​funktionsdeklarationen, og i tilfælde af en pilefunktion, mellem tegnet "=" og parenteserne.

Disse funktioner kan placeres i et objekt som metoder eller bruges 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 værd at huske på, at klassekonstruktører og gettere/sættere ikke kan være asynkrone.

Semantik og udførelsesregler

Asynkrone funktioner ligner grundlæggende JS-funktioner, men der er undtagelser.

Således returnerer asynkrone funktioner altid løfter:

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

Specifikt returnerer fn strengen hej. Nå, da dette er en asynkron funktion, er strengværdien pakket ind i et løfte ved hjælp af en konstruktør.

Her er et alternativt design uden Asynkron:

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

I dette tilfælde returneres løftet "manuelt". En asynkron funktion er altid pakket ind i et nyt løfte.

Hvis returværdien er en primitiv, returnerer async-funktionen værdien ved at pakke den ind i et løfte. Hvis returværdien er et løfteobjekt, returneres dets opløsning i et nyt løfte.

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

Men hvad sker der, hvis der er en fejl i en asynkron funktion?

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

Hvis det ikke behandles, vil foo() returnere et løfte med afvisning. I denne situation vil Promise.reject indeholdende en fejl blive returneret i stedet for Promise.resolve.

Asynkrone funktioner udsender altid et løfte, uanset hvad der returneres.

Asynkrone funktioner holder pause ved hver afventning.

Afvent påvirker udtryk. Så hvis udtrykket er et løfte, suspenderes asynkronfunktionen, indtil løftet er opfyldt. Hvis udtrykket ikke er et løfte, konverteres det til et løfte via Promise.resolve og afsluttes derefter.

// 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 af, hvordan fn-funktionen fungerer.

  • Efter at have kaldt det, konverteres den første linje fra const a = await 9; in const a = await Promise.resolve(9);.
  • Efter brug af Await suspenderes funktionsudførelsen, indtil a får sin værdi (i den aktuelle situation er den 9).
  • delayAndGetRandom(1000) sætter udførelsen af ​​fn-funktionen på pause, indtil den fuldfører sig selv (efter 1 sekund). Dette stopper effektivt fn-funktionen i 1 sekund.
  • delayAndGetRandom(1000) via resolve returnerer en tilfældig værdi, som derefter tildeles variablen b.
  • Tja, tilfældet med variabel c ligner tilfældet med variabel a. Derefter stopper alt et sekund, men nu returnerer delayAndGetRandom(1000) intet, fordi det ikke er påkrævet.
  • Som et resultat beregnes værdierne ved hjælp af formlen a + b * c. Resultatet er pakket ind i et løfte ved hjælp af Promise.resolve og returneres af funktionen.

Disse pauser kan minde om generatorer i ES6, men der er noget om det dine grunde.

Løsning af problemet

Nå, lad os nu se på løsningen på problemet nævnt ovenfor.

Funktionen finishMyTask bruger Await til at vente på resultaterne af operationer såsom queryDatabase, sendEmail, logTaskInFile og andre. Hvis du sammenligner denne løsning med den, hvor løfter blev brugt, vil lighederne blive tydelige. Async/Await-versionen forenkler dog i høj grad alle de syntaktiske kompleksiteter. I dette tilfælde er der ikke et stort antal tilbagekald og kæder som .then/.catch.

Her er en løsning med output af tal, der er to muligheder.

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, der bruger async-funktioner.

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

Fejl ved behandling

Ubehandlede fejl er pakket ind i et afvist løfte. Asynkrone funktioner kan dog bruge try/catch til at håndtere fejl 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 funktion, der enten lykkes ("perfekt tal") eller fejler med en fejl ("Beklager, tal for stort").

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

Da eksemplet ovenfor forventer, at canRejectOrReturn udføres, vil dets egen fejl resultere i udførelsen af ​​catch-blokken. Som et resultat vil funktionen foo ende med enten udefineret (når intet returneres i prøveblokken) eller med en fejl fanget. Som et resultat vil denne funktion ikke fejle, fordi try/catch vil håndtere selve funktionen foo.

Her er et andet eksempel:

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

Det er værd at være opmærksom på, at i eksemplet returneres canRejectOrReturn fra foo. Foo i dette tilfælde afsluttes enten med et perfekt tal eller returnerer en fejl ("Beklager, tal for stort"). Fangstblokken vil aldrig blive udført.

Problemet er, at foo returnerer løftet fra canRejectOrReturn. Så løsningen på foo bliver løsningen på canRejectOrReturn. I dette tilfælde vil koden kun bestå af to linjer:

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

Her er, hvad der sker, hvis I bruger afvent og vender tilbage sammen:

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

I koden ovenfor vil foo afslutte med succes med både et perfekt tal og en fejl fanget. Der vil ikke være nogen afslag her. Men foo vender tilbage med canRejectOrReturn, ikke med undefined. Lad os sørge for dette ved at fjerne return await canRejectOrReturn() linjen:

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

Almindelige fejl og faldgruber

I nogle tilfælde kan brug af Async/Await føre til fejl.

Glemt venter

Dette sker ret ofte - afvent nøgleordet er glemt før løftet:

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

Som du kan se, er der ingen ventetid eller retur i koden. Derfor forlader foo altid med udefineret uden 1 sekunds forsinkelse. Men løftet vil blive opfyldt. Hvis det giver en fejl eller afvisning, vil UnhandledPromiseRejectionWarning blive kaldt.

Asynkrone funktioner i tilbagekald

Async-funktioner bruges ret ofte i .map eller .filter som tilbagekald. Et eksempel er funktionen fetchPublicReposCount(brugernavn), som returnerer antallet af åbne repositories på GitHub. Lad os sige, at der er tre brugere, hvis metrics vi har brug for. Her er koden til denne opgave:

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 har brug for ArfatSalman, octocat, norvig konti. I dette tilfælde gør vi:

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

Det er værd at være opmærksom på Await i .map-tilbagekaldet. Her tæller er en række løfter, og .map er et anonymt tilbagekald for hver specificeret bruger.

Overdrevent konsekvent brug af afvent

Lad os tage denne kode 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 placeres repo-nummeret i tællevariablen, derefter tilføjes dette tal til counts-arrayet. Problemet med koden er, at indtil den første brugers data ankommer fra serveren, vil alle efterfølgende brugere være i standby-tilstand. Der behandles således kun én bruger ad gangen.

Hvis det for eksempel tager omkring 300 ms at behandle én bruger, så er det for alle brugere allerede et sekund; tidsforbruget afhænger lineært af antallet af brugere. Men da opnåelse af antallet af repo ikke afhænger af hinanden, kan processerne paralleliseres. Dette kræver arbejde 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 modtager en række løfter som input og returnerer et løfte. Sidstnævnte, efter at alle løfter i arrayet er gennemført eller ved den første afvisning, er fuldført. Det kan ske, at de ikke alle starter på samme tid - for at sikre samtidig start, kan du bruge p-map.

Konklusion

Asynkrone funktioner bliver stadig vigtigere for udvikling. Nå, til adaptiv brug af async-funktioner bør du bruge Asynkrone iteratorer. En JavaScript-udvikler bør være velbevandret i dette.

Skillbox anbefaler:

Kilde: www.habr.com

Tilføj en kommentar