Laten we Async/Await in JavaScript bekijken aan de hand van voorbeelden

De auteur van het artikel onderzoekt voorbeelden van Async/Await in JavaScript. Over het algemeen is Async/Await een handige manier om asynchrone code te schrijven. Voordat deze functie verscheen, werd dergelijke code geschreven met behulp van callbacks en beloften. De auteur van het originele artikel onthult de voordelen van Async/Await door verschillende voorbeelden te analyseren.

Herinnering: voor alle lezers van "Habr" - een korting van 10 roebel bij inschrijving voor een Skillbox-cursus met behulp van de promotiecode "Habr".

Skillbox beveelt aan: Educatieve online cursus "Java-ontwikkelaar".

Terugbellen

Terugbellen is een functie waarvan de oproep voor onbepaalde tijd wordt uitgesteld. Voorheen werden callbacks gebruikt in die codegebieden waar het resultaat niet onmiddellijk kon worden verkregen.

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

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

Er doen zich problemen voor wanneer u meerdere asynchrone bewerkingen tegelijk moet uitvoeren. Laten we ons dit scenario voorstellen: er wordt een verzoek ingediend bij de Arfat-gebruikersdatabase, u moet het profile_img_url-veld lezen en een afbeelding downloaden van de someserver.com-server.
Na het downloaden converteren wij de afbeelding naar een ander formaat, bijvoorbeeld van PNG naar JPEG. Als de conversie succesvol was, wordt er een brief naar het e-mailadres van de gebruiker gestuurd. Vervolgens wordt informatie over de gebeurtenis ingevoerd in het transformaties.log-bestand, met vermelding van de datum.

Het is de moeite waard om aandacht te besteden aan de overlap van callbacks en het grote aantal }) in het laatste deel van de code. Het heet Callback Hell of Pyramid of Doom.

De nadelen van deze methode liggen voor de hand:

  • Deze code is moeilijk te lezen.
  • Het is ook moeilijk om met fouten om te gaan, wat vaak leidt tot een slechte codekwaliteit.

Om dit probleem op te lossen zijn er beloftes toegevoegd aan JavaScript. Hiermee kunt u de diepe nesting van callbacks vervangen door het woord .then.

Het positieve aspect van beloften is dat ze de code veel beter leesbaar maken, van boven naar beneden in plaats van van links naar rechts. Beloften hebben echter ook hun problemen:

  • Je moet veel .then toevoegen.
  • In plaats van try/catch wordt .catch gebruikt om alle fouten af ​​te handelen.
  • Het werken met meerdere beloften binnen één lus is niet altijd handig; in sommige gevallen compliceren ze de code.

Hier is een probleem dat de betekenis van het laatste punt laat zien.

Stel dat we een for-lus hebben die een reeks getallen van 0 tot 10 afdrukt met willekeurige intervallen (0-n seconden). Met behulp van beloften moet je deze lus wijzigen, zodat de getallen in de juiste volgorde worden afgedrukt, van 0 tot en met 10. Dus als het zes seconden duurt om een ​​nul af te drukken en twee seconden om een ​​één af te drukken, moet eerst de nul worden afgedrukt, en dan het aftellen voor het afdrukken begint.

En natuurlijk gebruiken we Async/Await of .sort niet om dit probleem op te lossen. Een voorbeeldoplossing staat aan het einde.

Asynchrone functies

De toevoeging van asynchrone functies in ES2017 (ES8) vereenvoudigde de taak van het werken met beloften. Ik merk op dat asynchrone functies ‘bovenop’ beloften werken. Deze functies vertegenwoordigen geen kwalitatief verschillende concepten. Asynchrone functies zijn bedoeld als alternatief voor code die gebruik maakt van beloftes.

Async/Await maakt het mogelijk om werk met asynchrone code in synchrone stijl te organiseren.

Het kennen van beloften maakt het dus gemakkelijker om de principes van Async/Await te begrijpen.

syntaxis

Normaal gesproken bestaat het uit twee trefwoorden: async en await. Het eerste woord verandert de functie in asynchroon. Dergelijke functies maken het gebruik van wait mogelijk. In elk ander geval zal het gebruik van deze functie een fout genereren.

// 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 helemaal aan het begin van de functiedeclaratie ingevoegd, en in het geval van een pijlfunctie, tussen het teken “=” en de haakjes.

Deze functies kunnen als methoden in een object worden geplaatst of worden gebruikt in een klassendeclaratie.

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

Let op! Het is de moeite waard om te onthouden dat klasseconstructors en getters/setters niet asynchroon kunnen zijn.

Semantiek en uitvoeringsregels

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

Asynchrone functies retourneren dus altijd beloften:

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

Concreet retourneert fn de tekenreeks hallo. Omdat dit een asynchrone functie is, wordt de tekenreekswaarde verpakt in een belofte met behulp van een constructor.

Hier is een alternatief ontwerp zonder Async:

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

In dit geval wordt de belofte “handmatig” geretourneerd. Een asynchrone functie is altijd verpakt in een nieuwe belofte.

Als de geretourneerde waarde een primitief is, retourneert de async-functie de waarde door deze in een belofte te verpakken. Als de geretourneerde waarde een belofteobject is, wordt de resolutie ervan 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 in een asynchrone functie?

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

Als het niet wordt verwerkt, zal foo() een belofte met afwijzing retourneren. In deze situatie wordt Promise.reject met een fout geretourneerd in plaats van Promise.resolve.

Asynchrone functies leveren altijd een belofte op, ongeacht wat er wordt geretourneerd.

Asynchrone functies pauzeren bij elke wachttijd.

Wachten beïnvloedt uitdrukkingen. Dus als de uitdrukking een belofte is, wordt de asynchrone functie opgeschort totdat de belofte is vervuld. Als de expressie geen belofte is, wordt deze via Promise.resolve omgezet naar een belofte 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);

En hier is een beschrijving van hoe de fn-functie werkt.

  • Nadat deze is aangeroepen, wordt de eerste regel geconverteerd van const a = wait 9; in const a = wacht op Promise.resolve(9);.
  • Na het gebruik van Await wordt de uitvoering van de functie opgeschort totdat a zijn waarde krijgt (in de huidige situatie is dit 9).
  • delayAndGetRandom(1000) pauzeert de uitvoering van de fn-functie totdat deze zichzelf voltooit (na 1 seconde). Hierdoor stopt de fn-functie effectief gedurende 1 seconde.
  • delayAndGetRandom(1000) via solve retourneert een willekeurige waarde, die vervolgens wordt toegewezen aan de variabele b.
  • Welnu, het geval met variabele c is vergelijkbaar met het geval met variabele a. Daarna stopt alles een seconde, maar nu retourneert delayAndGetRandom(1000) niets omdat dit niet vereist is.
  • Als gevolg hiervan worden de waarden berekend met behulp van de formule a + b * c. Het resultaat wordt verpakt in een belofte met behulp van Promise.resolve en geretourneerd door de functie.

Deze pauzes doen misschien denken aan generatoren in ES6, maar er zit iets in jouw redenen.

Het probleem oplossen

Laten we nu eens kijken naar de oplossing voor het hierboven genoemde probleem.

De finishMyTask-functie gebruikt Await om te wachten op de resultaten van bewerkingen zoals queryDatabase, sendEmail, logTaskInFile en andere. Als je deze oplossing vergelijkt met die waarbij beloften werden gebruikt, zullen de overeenkomsten duidelijk worden. De Async/Await-versie vereenvoudigt echter alle syntactische complexiteiten aanzienlijk. In dit geval is er geen groot aantal callbacks en ketens zoals .then/.catch.

Hier is een oplossing met de uitvoer van getallen, er zijn twee opties.

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 asynchrone functies.

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

Fout bij verwerking

Onverwerkte fouten worden verpakt in een afgewezen belofte. Asynchrone functies kunnen echter try/catch gebruiken om fouten synchroon af te handelen.

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() is een asynchrone functie die slaagt (“perfect getal”) of mislukt met een fout (“Sorry, getal te groot”).

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

Omdat het bovenstaande voorbeeld verwacht dat canRejectOrReturn wordt uitgevoerd, zal zijn eigen fout resulteren in de uitvoering van het catch-blok. Het resultaat is dat de functie foo eindigt met ongedefinieerd (wanneer er niets wordt geretourneerd in het try-blok) of met een fout. Hierdoor zal deze functie niet falen omdat de try/catch de functie foo zelf zal afhandelen.

Hier is nog een voorbeeld:

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

Het is de moeite waard om op te letten dat in het voorbeeld canRejectOrReturn wordt geretourneerd door foo. Foo eindigt in dit geval met een perfect getal of retourneert een fout ("Sorry, getal te groot"). Het catch-blok zal nooit worden uitgevoerd.

Het probleem is dat foo de belofte retourneert die is doorgegeven door canRejectOrReturn. Dus de oplossing voor foo wordt de oplossing voor canRejectOrReturn. In dit geval bestaat de code uit slechts twee regels:

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

Dit gebeurt er als je await en return samen gebruikt:

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

In de bovenstaande code wordt foo succesvol afgesloten met zowel een perfect getal als een fout. Er zullen hier geen weigeringen zijn. Maar foo keert terug met canRejectOrReturn, niet met undefined. Laten we hier zeker van zijn door de return await canRejectOrReturn() regel te verwijderen:

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

Veel voorkomende fouten en valkuilen

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

Vergeten wachten

Dit gebeurt vrij vaak: het trefwoord 'wachten' wordt eerder vergeten dan de belofte:

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

Zoals u kunt zien, is er geen wachttijd of terugkeer in de code. Daarom sluit foo altijd af met ongedefinieerd zonder een vertraging van 1 seconde. Maar de belofte zal worden vervuld. Als er een fout of afwijzing optreedt, wordt UnhandledPromiseRejectionWarning aangeroepen.

Asynchrone functies bij terugbellen

Asynchrone functies worden vrij vaak gebruikt in .map of .filter als callbacks. Een voorbeeld is de functie fetchPublicReposCount(username), die het aantal open repository's op GitHub retourneert. Stel dat er drie gebruikers zijn waarvan we de statistieken nodig hebben. Hier is de code voor deze taak:

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

We hebben de rekeningen van ArfatSalman, Octocat en Norvig nodig. In dit geval doen wij:

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

Het is de moeite waard om op Await te letten in de .map-callback. Hier telt een reeks beloften, en .map is een anonieme callback voor elke opgegeven gebruiker.

Overmatig consistent gebruik van wachten

Laten we deze code als voorbeeld nemen:

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

Hier wordt het repo-nummer in de count-variabele geplaatst en vervolgens wordt dit nummer toegevoegd aan de counts-array. Het probleem met de code is dat totdat de gegevens van de eerste gebruiker van de server arriveren, alle volgende gebruikers in de standby-modus staan. Er wordt dus slechts één gebruiker tegelijk verwerkt.

Als het bijvoorbeeld ongeveer 300 ms duurt om één gebruiker te verwerken, dan is dit voor alle gebruikers al een seconde; de ​​bestede tijd is lineair afhankelijk van het aantal gebruikers. Maar aangezien het verkrijgen van het aantal repo's niet van elkaar afhankelijk is, kunnen de processen parallel worden gemaakt. Hiervoor is het werken met .map en Promise.all vereist:

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 reeks beloften als input en geeft een belofte terug. Dit laatste wordt, nadat alle beloften in de array zijn vervuld of bij de eerste afwijzing, vervuld. Het kan voorkomen dat ze niet allemaal tegelijkertijd starten. Om een ​​gelijktijdige start te garanderen, kunt u p-map gebruiken.

Conclusie

Asynchrone functies worden steeds belangrijker voor de ontwikkeling. Voor adaptief gebruik van asynchrone functies zou je moeten gebruiken Asynchrone iteratoren. Een JavaScript-ontwikkelaar moet hier goed thuis in zijn.

Skillbox beveelt aan:

Bron: www.habr.com

Voeg een reactie