Lad os tage et kig på Async/Await i JavaScript med eksempler

Artiklen forklarer med eksempler Async/Await i JavaScript. Generelt er Async/Await en bekvem måde at skrive asynkron kode på. Før denne funktion blev asynkron kode skrevet med callbacks og promises. Forfatteren af den originale artikel præsenterer fordelene ved Async/Await ved at gennemgå forskellige eksempler.

Vi minder om: til alle læsere af 'Habr' — rabat på 10.000 rubler ved tilmelding til et hvilket som helst Skillbox-kursus med rabatkoden 'Habr'.

Skillbox anbefaler: Online uddannelseskursus «Java-udvikler».

Callback

En Callback er en funktion, hvis kald er forsinket i ubestemt tid. Tidligere blev callbacks brugt i de dele af koden, hvor resultatet ikke kunne opnås med det samme.

Her er et eksempel på asynkront fil-læsning i Node.js:

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

Problemer opstår, når der skal udføres flere asynkrone operationer samtidig. Lad os forestille os et scenarie, hvor der foretages en forespørgsel til databasen for brugeren Arfat, og hans felt profile_img_url skal læses og billedet skal indlæses fra serveren someserver.com.
Efter upload konverterer vi billedet til et andet format, for eksempel fra PNG til JPEG. Hvis konverteringen lykkes, sendes en e-mail til brugeren. Derefter registreres informationen om hændelsen i transformations.log-filen sammen med datoen.

Det skal bemærkes, at indlejring af callbacks og det store antal }) i den endelige del af koden skaber en udfordring. Dette kaldes Callback Hell eller Pyramid of Doom.

Ulemperne ved denne tilgang er åbenlyse:

  • Koden er svær at læse.
  • Det er også svært at håndtere fejl, hvilket ofte fører til en forringelse af kodekvaliteten.

For at løse dette problem blev promises tilføjet til JavaScript. De gør det muligt at erstatte den dybe indlejring af callbacks med .then.

En positiv ting ved promises er, at koden dermed bliver meget lettere at læse, idet den læses ovenfra og ned, snarere end venstre mod højre. Alligevel har promises også deres egne problemer:

  • Der er behov for at tilføje mange .then.
  • I stedet for try/catch bruges .catch til at håndtere alle fejl.
  • At arbejde med flere promises inden for en enkelt cyklus er ikke altid praktisk, og i nogle tilfælde gør de koden mere kompleks.

Her er en opgave, der viser værdien af det sidste punkt.

Antag at der er en for-løkke, der udskriver en sekvens af tal fra 0 til 10 med et tilfældigt interval (0–n sekunder). Ved at bruge løfter skal denne løkke ændres, så tallene udskrives i rækkefølge fra 0 til 10. Så hvis udskrivningen af nul tager 6 sekunder, og én tager 2 sekunder, skal nul udskrives først, hvorefter tælleren for udskrivningen af én begynder.

Og selvfølgelig bruger vi ikke Async/Await eller .sort til at løse denne opgave. Eksemplet på løsning findes til sidst.

Async-funktioner

Tilføjelsen af async-funktioner i ES2017 (ES8) har gjort arbejdet med løfter lettere. Jeg vil bemærke, at async-funktioner fungerer "oven på" løfter. Disse funktioner repræsenterer ikke kvalitativt forskellige koncepter. Async-funktioner er designet som et alternativ til koden, der bruger løfter.

Async/Await giver mulighed for at organisere arbejdet med asynkron kode i synkron stil.

Dermed letter kendskabet til løfter forståelsen af principperne bag Async/Await.

Syntaks

I en normal situation består det af to nøgleord: async og await. Det første ord gør funktionen asynkron. I sådanne funktioner er det tilladt at bruge await. I alle andre tilfælde vil brugen af denne funktion give 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 i begyndelsen af funktionsdeklarationen, og i tilfælde af brug af en pilfunktion - mellem lighedstegnet «=» og parenteserne.

Disse funktioner kan placeres i et objekt som metoder eller bruges i klassens deklaration.

// 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, at klassekonstruktører og gettere/settere ikke kan være asynkrone.

Semantik og udførelsesregler

Async-funktioner ligner i princippet standard JS-funktioner, men der er undtagelser.

Async-funktioner returnerer altid løfter:

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

Specifikt returnerer fn strengen hello. Og da dette er en asynkron funktion, bliver strengen indkapslet i et løfte ved hjælp af konstruktøren.

Her er en alternativ konstruktion uden Async:

function fn() {
  return Promise.resolve('hello');
}

fn().then(console.log);
// hello

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

I tilfælde af at den returnerede værdi er en primitive, returnerer en async-funktion værdien indkapslet i et løfte. Hvis den returnerede værdi derimod allerede er et løfteobjekt, returneres dets resultat 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 opstår en fejl inden i den asynkrone funktion?

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

Hvis den ikke bliver håndteret, vil foo() returnere et løfte med en afvisning. I denne situation vil Promise.reject blive returneret i stedet for Promise.resolve, som indeholder fejlen.

Async-funktioner returnerer altid et løfte, uanset hvad der returneres.

Asynkrone funktioner pauser ved hvert await.

Await påvirker udtryk. Hvis udtrykket er et løfte, pauser async-funktionen indtil løftet er udført. Hvis udtrykket ikke er et løfte, konverteres det til et løfte gennem Promise.resolve og derefter færdiggø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);

Her er en beskrivelse af, hvordan fn-funktionen fungerer.

  • Efter dens opkald konverteres den første linje fra const a = await 9; til const a = await Promise.resolve(9);.
  • Efter brug af Await pauses funktionen, indtil a får sin værdi (i dette tilfælde 9).
  • delayAndGetRandom(1000) pauser udførelsen af fn-funktionen, indtil den er afsluttet (efter 1 sekund). Dette er faktisk, som at stoppe fn-funktionen i 1 sekund.
  • delayAndGetRandom(1000) returnerer et tilfældigt værdi gennem resolve, som derefter tildeles variablen b.
  • Og situationen med variablen c ligner situationen med variablen a. Efter dette stopper alt i et sekund, men nu returnerer delayAndGetRandom(1000) intet, da det ikke er nødvendigt.
  • Til sidst beregnes værdierne efter formlen a + b * c. Resultatet indkapsles 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 sine grunde.

At løse opgaven

Nu lad os se på løsningen af den opgave, der blev nævnt tidligere.

I funktionen finishMyTask bruges Await til at vente på resultaterne af operationer som queryDatabase, sendEmail, logTaskInFile og andre. Hvis man derimod sammenligner denne løsning med den, der bruger promises, bliver ligheden tydelig. Alligevel forenkler versionen med Async/Await ret meget de synaktiske kompleksiteter. I dette tilfælde er der ikke mange callbacks og kæder som .then/.catch.

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

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

Error handling

Ubehandlede fejl indkapsles i en rejected promise. Alligevel kan man i async-funktioner bruge try/catch-konstruktionen til at udføre synkron fejlhåndtering.

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

canRejectOrReturn() er en asynkron funktion, der enten udføres med succes ('perfekt nummer') eller ikke lykkes med en fejl ('Beklager, tallet er for stort').

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'fejl indfanget';
  }
}

Da canRejectOrReturn forventes at blive udført i eksemplet ovenfor, vil dens egne mislykkede lukning medføre, at catch-blokken udføres. Resultatet vil være, at funktionen foo afsluttes enten med undefined (når der ikke returneres noget i try-blokken) eller med 'fejl indfanget'. I sidste ende får denne funktion ikke en mislykket lukning, da try/catch vil håndtere funktionen foo.

Her er endnu et eksempel:

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

Det er vigtigt at bemærke, at i eksemplet fra foo returneres canRejectOrReturn. Foo afsluttes i dette tilfælde enten med 'perfekt nummer' eller med en fejl ('Beklager, tallet er for stort'). Catch-blokken vil aldrig blive udført.

Problemet er, at foo returnerer en promise, der er videresendt fra canRejectOrReturn. Derfor bliver løsningen af funktionen foo også løsningen for canRejectOrReturn. I dette tilfælde vil koden kun bestå af to linjer:

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

Og hvad sker der, hvis man bruger await og return sammen:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'fejl indfanget';
  }
}

I koden ovenfor afsluttes foo med både 'perfekt nummer' og 'fejl indfanget'. Her vil der ikke være mislykkede lukninger. Men foo vil afsluttes med canRejectOrReturn, og ikke med undefined. Lad os dobbelttjekke dette ved at fjerne linjen 'return await canRejectOrReturn():'

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

Almindelige fejl og faldgruber

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

Glemt await

Dette sker ret ofte — man glemmer nøgleordet await før et løfte:

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

I koden er der hverken await eller return at se. Derfor afsluttes foo altid med undefined uden en forsinkelse på 1 sekund. Men løftet vil blive udført. Hvis det i stedet giver en fejl eller afvises, vil UnhandledPromiseRejectionWarning blive kaldt.

Async-funktioner i callbacks

Async-funktioner bruges ofte i .map eller .filter som callbacks. Et eksempel kan være funktionen fetchPublicReposCount(username), som returnerer antallet af åbne GitHub-repositorier. Antag, at vi har tre brugere, hvis data 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 konti ArfatSalman, octocat, norvig. I dette tilfælde udfører vi:

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

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

Bemærk opmærksomheden på Await i callback'en .map. Her er counts et array af løfter, og .map er en anonym callback for hver angivet bruger.

Overdreven sekventiel brug af await

Som eksempel kan vi tage følgende kode:

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 antallet af repoer i variablen count, som derefter tilføjes til arrayet counts. Problemet med koden er, at mens der venters på, at dataene fra den første bruger kommer ind, vil alle efterfølgende brugere være i ventetilstand. Dermed behandles kun én bruger ad gangen.

Hvis det for eksempel tager omkring 300 ms at behandle én bruger, så bliver det til en sekund for alle brugere; den tid, der bruges, afhænger lineært af antallet af brugere. Men da hentningen af antallet af repoer ikke afhænger af hinanden, kan processerne køre parallelt. Til dette skal man 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 tager et array af promises som input og returnerer en promise. Den afsluttes efter at alle promises i arrayet er færdige, eller ved den første rejection. Det kan ske, at ikke alle promises kører samtidig - for at sikre samtidig kørsel kan man bruge p-map.

Konklusion

Async-funktioner bliver stadigt vigtigere for udvikling. For at kunne udnytte async-funktioner effektivt, skal man benytte sig af Async Iterators. En JavaScript-udvikler skal have en god forståelse for dette.

Skillbox anbefaler:

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster