We bespreken Async/Await in JavaScript aan de hand van voorbeelden

De auteur van het artikel onderzoekt aan de hand van voorbeelden Async/Await in JavaScript. Over het algemeen is Async/Await een handige manier om asynchrone code te schrijven. Voor de komst van deze functionaliteit werd dergelijke code geschreven met behulp van callbacks en promises. De auteur van het originele artikel onthult de voordelen van Async/Await door verschillende voorbeelden te bespreken.

Ter herinnering: voor alle lezers van «Habr» — een korting van 10.000 roebel bij inschrijving voor elke cursus van Skillbox met de promocode «Habr».

Skillbox raadt aan: Online opleidingscursus «Java-ontwikkelaar».

Callback

Een callback is een functie waarvan de aanroep is uitgesteld naar een onbepaalde tijd. Vroeger werden callbacks gebruikt in die delen van de code waar het resultaat niet onmiddellijk kon worden verkregen.

Hier is een voorbeeld van asynchroon een bestand lezen in Node.js:

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

Problemen ontstaan wanneer er meerdere asynchrone bewerkingen tegelijk moeten worden uitgevoerd. Laten we ons het volgende scenario voorstellen: een verzoek wordt gedaan in de database voor gebruiker Arfat, we moeten zijn veld profile_img_url lezen en de afbeelding van de server someserver.com laden.
Na het uploaden converteren we de afbeelding naar een ander formaat, bijvoorbeeld van PNG naar JPEG. Als de conversie succesvol is, wordt er een e-mail naar de gebruiker gestuurd. Vervolgens worden de details van de gebeurtenis vastgelegd in het bestand transformations.log met de datum.

Het is belangrijk om op te merken dat de overlappendheid van callbacks en het grote aantal }) aan het einde van de code een probleem vormt. Dit wordt Callback Hell of Pyramid of Doom genoemd.

De nadelen van deze aanpak zijn duidelijk:

  • Deze code is moeilijk leesbaar.
  • Het is ook moeilijk om fouten af te handelen, wat vaak leidt tot een slechtere kwaliteit van de code.

Om dit probleem op te lossen, zijn er promises aan JavaScript toegevoegd. Ze maken het mogelijk om de diepe nesteling van callbacks te vervangen door het woord .then.

Een positief aspect van promises is dat de code veel beter leesbaar is, van boven naar beneden, in plaats van van links naar rechts. Toch hebben promises ook hun eigen problemen:

  • Er moeten veel .then toegevoegd worden.
  • In plaats van try/catch wordt .catch gebruikt voor het afhandelen van alle fouten.
  • Werken met meerdere promises in één cyclus is niet altijd handig; in sommige gevallen maakt het de code complexer.

Hier is een taak die de waarde van het laatste punt illustreert.

Stel je voor dat er een for-lus is die een reeks getallen van 0 tot 10 afdrukt met een willekeurige interval (0–n seconden). Met behulp van beloftes moet deze lus worden aangepast zodat de getallen in volgorde van 0 tot 10 worden afgedrukt. Als de uitvoering van nul bijvoorbeeld 6 seconden duurt en van één 2 seconden, moet eerst nul worden afgedrukt en daarna pas de uitvoering van één beginnen.

En natuurlijk gebruiken we voor deze taak geen Async/Await of .sort. Een voorbeeld van de oplossing is aan het einde te vinden.

Async-functies

De toevoeging van async-functies in ES2017 (ES8) heeft het werken met beloftes vereenvoudigd. Ik wil benadrukken dat async-functies ‘bovenop’ beloftes werken. Deze functies vormen geen kwalitatief andere concepten. Async-functies zijn ontworpen als een alternatief voor code die beloftes gebruikt.

Async/Await maakt het mogelijk om te werken met asynchrone code in een synchrone stijl.

Derhalve vergemakkelijkt kennis van beloftes het begrijpen van de principes van Async/Await.

Syntax

In een standaard situatie bestaat het uit twee sleutelwoorden: async en await. Het eerste woord maakt de functie asynchroon. In dergelijke functies is het gebruik van await toegestaan. In elke andere situatie zal het gebruik van deze functie een fout veroorzaken.

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

Async wordt aan het begin van de functieaankondiging geplaatst, en in het geval van een pijl-functie tussen het gelijkteken en de haakjes.

Deze functies kunnen worden geplaatst in een object als methoden of worden gebruikt in een klasse-aankondiging.

// 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! Het is belangrijk te onthouden dat klasseconstructors en getters/setters niet asynchroon kunnen zijn.

Semantiek en uitvoeringsregels

Async-functies zijn in principe vergelijkbaar met standaard JS-functies, maar er zijn uitzonderingen.

Async-functies retourneren altijd beloftes:

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

Specifiek retourneert fn de string hello. Aangezien dit een asynchrone functie is, wordt de waarde van de string verpakt in een belofte met behulp van de constructor.

Hier is een alternatieve constructie zonder Async:

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

In dit geval wordt het retourneren van de belofte ‘handmatig’ uitgevoerd. Een asynchrone functie wordt altijd verpakt in een nieuwe belofte.

In het geval dat de geretourneerde waarde een primitief is, retourneert de async-functie de waarde door deze in een belofte te wikkelen. In het geval dat de geretourneerde waarde een belofte-object is, wordt de oplossing geretourneerd in een nieuwe belofte.

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

Maar wat gebeurt er als er een fout optreedt binnen de asynchrone functie?

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

Als deze niet wordt afgehandeld, retourneert foo() een belofte met een afwijzing. In dit geval wordt in plaats van Promise.resolve Promise.reject geretourneerd, dat de fout bevat.

Async-functies geven altijd een belofte terug, ongeacht wat er wordt geretourneerd.

Asynchrone functies worden gepauzeerd bij elke await.

Await beïnvloedt uitdrukkingen. Als de uitdrukking een belofte is, wordt de async-functie gepauzeerd totdat de belofte is voltooid. Als de uitdrukking geen belofte is, wordt deze omgezet in een belofte via Promise.resolve en vervolgens voltooid.

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

Hier is een beschrijving van hoe de fn-functie werkt.

  • Na de aanroep wordt de eerste regel omgezet van const a = await 9; naar const a = await Promise.resolve(9);.
  • Na het gebruik van Await wordt de uitvoering van de functie gepauzeerd totdat a zijn waarde (in dit geval 9) ontvangt.
  • delayAndGetRandom(1000) pauzeert de uitvoering van de fn-functie totdat deze zelf is voltooid (na 1 seconde). Dit is feitelijk de pauze van de fn-functie gedurende 1 seconde.
  • delayAndGetRandom(1000) retourneert via resolve een willekeurige waarde, die vervolgens aan variabele b wordt toegewezen.
  • De situatie met variabele c is vergelijkbaar met de situatie met variabele a. Daarna wordt alles een seconde gepauzeerd, maar nu retourneert delayAndGetRandom(1000) niets, omdat dat niet nodig is.
  • Uiteindelijk worden de waarden berekend met de formule a + b * c. Het resultaat wordt vervolgens in een belofte gewikkeld met behulp van Promise.resolve en geretourneerd door de functie.

Deze pauzes kunnen doen denken aan generatoren in ES6, maar daar zijn redenen voor.

De taak aanpakt

Laten we nu eens kijken naar de oplossing van de taak die hierboven is aangegeven.

In de functie finishMyTask wordt Await gebruikt om te wachten op de resultaten van operaties zoals queryDatabase, sendEmail, logTaskInFile en anderen. Wanneer we deze oplossing vergelijken met die waarbij gebruik werd gemaakt van promesses, wordt de overeenkomst duidelijk. Toch vereenvoudigt de versie met Async/Await de syntactische complexiteit aanzienlijk. In dit geval zijn er geen vele callbacks en ketens zoals .then/.catch.

Hier is een oplossing voor het afdrukken van getallen, hier zijn twee varianten.

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

En hier is een oplossing met behulp van async-functies.

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

Foutafhandeling

Onbehandelde fouten worden verpakt in een rejected promise. Toch kunnen we in async-functies de try/catch-constructie gebruiken voor synchrone foutafhandeling.

async function canRejectOrReturn() {
  // wacht een seconde
  await new Promise(res => setTimeout(res, 1000));
 
// Weiger met ~50% kans
  if (Math.random() > 0.5) {
    throw new Error('Sorry, getal te groot.')
  }
 
return 'perfect getal';
}

canRejectOrReturn() is een asynchrone functie die ofwel succesvol wordt uitgevoerd (“perfect getal”), ofwel mislukt met een fout (“Sorry, getal te groot”).

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'fout gevangen';
  }
}

Aangezien in het bovenstaande voorbeeld canRejectOrReturn wordt verwacht, zal een eigen mislukte uitvoering de catch-blok uitvoeren. Als resultaat kan de functie foo eindigen met undefined (wanneer er niets wordt geretourneerd in het try-blok), of met 'fout gevangen'. Hierdoor zal deze functie niet mislukt zijn, omdat try/catch de foutafhandeling van de functie foo op zich neemt.

Hier is nog een voorbeeld:

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

Het is belangrijk om op te merken dat in het voorbeeld van foo canRejectOrReturn wordt geretourneerd. Foo eindigt in dit geval ofwel met perfect getal, ofwel met de fout Error (“Sorry, getal te groot”). Het catch-blok zal nooit worden uitgevoerd.

Het probleem is dat foo een promise retourneert die van canRejectOrReturn is doorgegeven. Daarom wordt de oplossing van de functie foo de oplossing voor canRejectOrReturn. In dit geval zal de code uit slechts twee regels bestaan:

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

En wat gebeurt er als we await en return samen gebruiken:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'fout gevangen';
  }
}

In de bovenstaande code zal foo zowel succesvol eindigen met perfect getal als met 'fout gevangen'. Er zullen geen mislukte uitvoeringen zijn. Maar foo zal eindigen met canRejectOrReturn, en niet met undefined. Laten we dit verifiëren door de regel return await canRejectOrReturn() te verwijderen:

proberen {
    const waarde = await kanAfwijzenOfTeruggeven();
    return waarde;
}
// …

Veelvoorkomende fouten en valkuilen

In sommige gevallen kan het gebruik van Async/Await leiden tot fouten.

Vergeten await

Dit komt vrij vaak voor — het sleutelwoord await wordt vergeten voor de belofte:

async functie foo() {
  proberen {
    kanAfwijzenOfTeruggeven();
  } vangen (e) {
    return 'gevangen';
  }
}

In de code, zoals te zien is, is er geen await of return. Daarom eindigt foo altijd met undefined zonder een vertraging van 1 seconde. Maar de belofte zal worden uitgevoerd. Als deze een fout of een afwijzing genereert, zal dit een UnhandledPromiseRejectionWarning oproepen.

Async-functies in callback-functies

Async-functies worden vaak gebruikt in .map of .filter als callbacks. Een voorbeeld is de functie fetchPublicReposCount(username), die het aantal openbare repositories op GitHub retourneert. Stel dat we de statistieken van drie gebruikers nodig hebben. Hier is de code voor deze taak:

const url = 'https://api.github.com/users';

// Hulpfunctie om repo-tellingen op te halen
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

We hebben de accounts ArfatSalman, octocat, norvig nodig. In dit geval voeren we uit:

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

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

Let op het Await in de callback van .map. Hier is counts een array van beloftes, en .map is een anonieme callback voor elke opgegeven gebruiker.

Overmatige sequentiële gebruik van await

Als voorbeeld nemen we deze code:

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

Hier wordt het aantal repo's in de variabele count geplaatst, waarna dit aantal aan de array counts wordt toegevoegd. Het probleem van de code is dat tot de eerste gebruiker gegevens van de server komen, alle volgende gebruikers in afwachting zijn. Op ieder moment wordt er dus slechts één gebruiker verwerkt.

Als het bijvoorbeeld ongeveer 300 ms kost om één gebruiker te verwerken, dan duurt dat voor alle gebruikers al een seconde, de tijd die wordt besteed, is lineair afhankelijk van het aantal gebruikers. Maar aangezien het ophalen van het aantal repo's niet van elkaar afhankelijk is, kunnen de processen parallel worden uitgevoerd. Hiervoor moet er gewerkt worden met .map en 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 ontvangt een array van beloftes die een belofte retourneren. Het eindigt pas wanneer alle beloftes in de array zijn voltooid of bij de eerste afwijzing. Het kan gebeuren dat ze niet allemaal tegelijk worden gestart — om gelijktijdige uitvoering te garanderen, kan p-map worden gebruikt.

Conclusie

Async-functies worden steeds belangrijker in de ontwikkeling. Voor adaptief gebruik van async-functies is het de moeite waard om gebruik te maken van Async Iterators. Een JavaScript-ontwikkelaar moet hier goed in thuis zijn.

Skillbox raadt aan:

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster