Schauen wir uns Async/Await in JavaScript anhand von Beispielen an

Der Autor des Artikels untersucht Beispiele für Async/Await in JavaScript. Insgesamt ist Async/Await eine bequeme Möglichkeit, asynchronen Code zu schreiben. Bevor diese Funktion verfügbar war, wurde solcher Code mithilfe von Rückrufen und Versprechen geschrieben. Der Autor des Originalartikels verdeutlicht die Vorteile von Async/Await anhand der Analyse verschiedener Beispiele.

Erinnerung: für alle Leser von „Habr“ – ein Rabatt von 10 Rubel bei der Anmeldung zu einem beliebigen Skillbox-Kurs mit dem Aktionscode „Habr“.

Skillbox empfiehlt: Pädagogischer Online-Kurs "Java Entwickler".

Rückruf

Callback ist eine Funktion, deren Aufruf auf unbestimmte Zeit verzögert wird. Bisher wurden Rückrufe in den Codebereichen verwendet, in denen das Ergebnis nicht sofort erhalten werden konnte.

Hier ist ein Beispiel für das asynchrone Lesen einer Datei in Node.js:

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

Probleme treten auf, wenn mehrere asynchrone Vorgänge gleichzeitig ausgeführt werden müssen. Stellen wir uns dieses Szenario vor: Es wird eine Anfrage an die Arfat-Benutzerdatenbank gestellt. Sie müssen das Feld „profile_img_url“ lesen und ein Bild vom Server „someserver.com“ herunterladen.
Nach dem Download konvertieren wir das Bild in ein anderes Format, beispielsweise von PNG nach JPEG. Wenn die Konvertierung erfolgreich war, wird ein Brief an die E-Mail-Adresse des Benutzers gesendet. Als nächstes werden Informationen über das Ereignis unter Angabe des Datums in die Datei transformations.log eingegeben.

Es lohnt sich, auf die Überlappung der Rückrufe und die große Anzahl von }) im letzten Teil des Codes zu achten. Es heißt Callback Hell oder Pyramid of Doom.

Die Nachteile dieser Methode liegen auf der Hand:

  • Dieser Code ist schwer zu lesen.
  • Außerdem ist es schwierig, mit Fehlern umzugehen, was häufig zu einer schlechten Codequalität führt.

Um dieses Problem zu lösen, wurden Versprechen zu JavaScript hinzugefügt. Sie ermöglichen es Ihnen, tiefe Verschachtelungen von Rückrufen durch das Wort .then zu ersetzen.

Der positive Aspekt von Versprechen besteht darin, dass sie den Code viel besser lesbar machen, und zwar von oben nach unten statt von links nach rechts. Allerdings haben Versprechen auch ihre Probleme:

  • Sie müssen viel .then hinzufügen.
  • Anstelle von try/catch wird .catch zur Behandlung aller Fehler verwendet.
  • Das Arbeiten mit mehreren Versprechen innerhalb einer Schleife ist nicht immer bequem; in einigen Fällen verkomplizieren sie den Code.

Hier ist ein Problem, das die Bedeutung des letzten Punktes zeigt.

Angenommen, wir haben eine for-Schleife, die in zufälligen Abständen (0–n Sekunden) eine Folge von Zahlen von 10 bis 0 ausgibt. Wenn Sie Versprechen verwenden, müssen Sie diese Schleife so ändern, dass die Zahlen der Reihe nach von 0 bis 10 gedruckt werden. Wenn es also 6 Sekunden dauert, eine Null zu drucken, und 2 Sekunden, um eine Eins zu drucken, sollte zuerst die Null und dann gedruckt werden Der Countdown zum Drucken beginnt.

Und natürlich verwenden wir weder Async/Await noch .sort, um dieses Problem zu lösen. Eine Beispiellösung finden Sie am Ende.

Asynchrone Funktionen

Das Hinzufügen asynchroner Funktionen in ES2017 (ES8) vereinfachte die Arbeit mit Versprechen. Ich stelle fest, dass asynchrone Funktionen „zusätzlich“ zu Versprechen funktionieren. Diese Funktionen stellen keine qualitativ unterschiedlichen Konzepte dar. Asynchrone Funktionen sind als Alternative zu Code gedacht, der Versprechen verwendet.

Async/Await ermöglicht es, die Arbeit mit asynchronem Code im synchronen Stil zu organisieren.

Daher ist es durch die Kenntnis von Versprechen einfacher, die Prinzipien von Async/Await zu verstehen.

Syntax

Normalerweise besteht es aus zwei Schlüsselwörtern: async undawait. Das erste Wort macht die Funktion asynchron. Solche Funktionen ermöglichen die Verwendung vonawait. In allen anderen Fällen führt die Verwendung dieser Funktion zu einem Fehler.

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

Async wird ganz am Anfang der Funktionsdeklaration und im Fall einer Pfeilfunktion zwischen dem „=“-Zeichen und den Klammern eingefügt.

Diese Funktionen können als Methoden in einem Objekt platziert oder in einer Klassendeklaration verwendet werden.

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

ACHTUNG! Denken Sie daran, dass Klassenkonstruktoren und Getter/Setter nicht asynchron sein können.

Semantik und Ausführungsregeln

Asynchrone Funktionen ähneln grundsätzlich den Standard-JS-Funktionen, es gibt jedoch Ausnahmen.

Daher geben asynchrone Funktionen immer Versprechen zurück:

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

Konkret gibt fn die Zeichenfolge „Hallo“ zurück. Da es sich um eine asynchrone Funktion handelt, wird der Zeichenfolgenwert mithilfe eines Konstruktors in ein Versprechen verpackt.

Hier ist ein alternatives Design ohne Async:

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

In diesem Fall wird das Versprechen „manuell“ zurückgegeben. Eine asynchrone Funktion ist immer in ein neues Versprechen verpackt.

Wenn der Rückgabewert ein Grundelement ist, gibt die asynchrone Funktion den Wert zurück, indem sie ihn in ein Versprechen einschließt. Wenn der Rückgabewert ein Versprechensobjekt ist, wird seine Auflösung in einem neuen Versprechen zurückgegeben.

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

Aber was passiert, wenn in einer asynchronen Funktion ein Fehler auftritt?

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

Wenn es nicht verarbeitet wird, gibt foo() ein Versprechen mit Ablehnung zurück. In dieser Situation wird Promise.reject mit einem Fehler anstelle von Promise.resolve zurückgegeben.

Asynchrone Funktionen geben immer ein Versprechen aus, unabhängig davon, was zurückgegeben wird.

Asynchrone Funktionen werden bei jedem „await“ angehalten.

Warten wirkt sich auf Ausdrücke aus. Wenn der Ausdruck also ein Versprechen ist, wird die asynchrone Funktion angehalten, bis das Versprechen erfüllt ist. Wenn der Ausdruck kein Versprechen ist, wird er über Promise.resolve in ein Versprechen umgewandelt und dann vervollständigt.

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

Und hier ist eine Beschreibung, wie die Fn-Funktion funktioniert.

  • Nach dem Aufruf wird die erste Zeile von const a = waiting 9; in const a = Warten auf Promise.resolve(9);.
  • Nach der Verwendung von Await wird die Funktionsausführung angehalten, bis a seinen Wert erhält (in der aktuellen Situation ist er 9).
  • „delayAndGetRandom(1000)“ unterbricht die Ausführung der Fn-Funktion, bis diese abgeschlossen ist (nach 1 Sekunde). Dadurch wird die Fn-Funktion effektiv für 1 Sekunde gestoppt.
  • „delayAndGetRandom(1000)“ über „resolve“ gibt einen Zufallswert zurück, der dann der Variablen b zugewiesen wird.
  • Nun, der Fall mit der Variablen c ähnelt dem Fall mit der Variablen a. Danach stoppt alles für eine Sekunde, aber jetzt gibt „delayAndGetRandom(1000)“ nichts zurück, da es nicht erforderlich ist.
  • Als Ergebnis werden die Werte nach der Formel a + b * c berechnet. Das Ergebnis wird mithilfe von Promise.resolve in ein Versprechen verpackt und von der Funktion zurückgegeben.

Diese Pausen erinnern vielleicht an Generatoren in ES6, aber da ist etwas dran Deine Gründe.

Lösung des Problems

Schauen wir uns nun die Lösung des oben genannten Problems an.

Die Funktion „finishMyTask“ verwendet „Await“, um auf die Ergebnisse von Vorgängen wie „queryDatabase“, „sendEmail“, „logTaskInFile“ und anderen zu warten. Wenn Sie diese Lösung mit der Lösung vergleichen, bei der Versprechen verwendet wurden, werden die Ähnlichkeiten offensichtlich. Die Async/Await-Version vereinfacht jedoch alle syntaktischen Komplexitäten erheblich. In diesem Fall gibt es keine große Anzahl von Rückrufen und Ketten wie .then/.catch.

Hier ist eine Lösung mit der Ausgabe von Zahlen, es gibt zwei Möglichkeiten.

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

Und hier ist eine Lösung mit asynchronen Funktionen.

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

Fehlerbearbeitung

Nicht behandelte Fehler werden in ein abgelehntes Versprechen verpackt. Allerdings können asynchrone Funktionen Try/Catch verwenden, um Fehler synchron zu behandeln.

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() ist eine asynchrone Funktion, die entweder erfolgreich ist („perfekte Zahl“) oder mit einem Fehler fehlschlägt („Leider ist die Zahl zu groß“).

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

Da das obige Beispiel die Ausführung von canRejectOrReturn erwartet, führt sein eigener Fehler zur Ausführung des Catch-Blocks. Infolgedessen endet die Funktion foo entweder mit undefiniert (wenn im Try-Block nichts zurückgegeben wird) oder mit einem abgefangenen Fehler. Daher schlägt diese Funktion nicht fehl, da try/catch die Funktion foo selbst verarbeitet.

Hier ist ein weiteres Beispiel:

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

Es lohnt sich, darauf zu achten, dass im Beispiel canRejectOrReturn von foo zurückgegeben wird. In diesem Fall endet Foo entweder mit einer perfekten Zahl oder gibt einen Fehler zurück („Leider ist die Zahl zu groß“). Der Catch-Block wird niemals ausgeführt.

Das Problem besteht darin, dass foo das von canRejectOrReturn übergebene Versprechen zurückgibt. Die Lösung für foo wird also zur Lösung für canRejectOrReturn. In diesem Fall besteht der Code nur aus zwei Zeilen:

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

Folgendes passiert, wenn Sie „await“ und „return“ zusammen verwenden:

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

Im obigen Code wird foo erfolgreich mit einer perfekten Zahl und einem abgefangenen Fehler beendet. Hier wird es keine Absagen geben. Aber foo wird mit canRejectOrReturn zurückkehren, nicht mit undefiniert. Stellen wir dies sicher, indem wir die Return-Await-CanRejectOrReturn()-Zeile entfernen:

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

Häufige Fehler und Fallstricke

In einigen Fällen kann die Verwendung von Async/Await zu Fehlern führen.

Vergessenes Warten

Das passiert ziemlich oft – das Schlüsselwort „await“ wird vor dem Versprechen vergessen:

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

Wie Sie sehen, gibt es im Code kein Warten oder Zurückkehren. Daher wird foo immer mit undefiniert ohne eine Verzögerung von 1 Sekunde beendet. Aber das Versprechen wird erfüllt. Wenn ein Fehler oder eine Ablehnung ausgelöst wird, wird UnhandledPromiseRejectionWarning aufgerufen.

Asynchrone Funktionen in Rückrufen

Asynchrone Funktionen werden in .map oder .filter häufig als Rückrufe verwendet. Ein Beispiel ist die Funktion fetchPublicReposCount(username), die die Anzahl der offenen Repositories auf GitHub zurückgibt. Nehmen wir an, es gibt drei Benutzer, deren Metriken wir benötigen. Hier ist der Code für diese Aufgabe:

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

Wir brauchen Konten von ArfatSalman, Octocat und Norvig. In diesem Fall machen wir:

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

Es lohnt sich, im .map-Rückruf auf Await zu achten. Hier zählt eine Reihe von Versprechen, und .map ist ein anonymer Rückruf für jeden angegebenen Benutzer.

Übermäßig konsistente Verwendung von „await“.

Nehmen wir diesen Code als Beispiel:

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 wird die Repo-Nummer in die Zählvariable eingefügt, dann wird diese Zahl zum Zählarray hinzugefügt. Das Problem mit dem Code besteht darin, dass sich alle nachfolgenden Benutzer im Standby-Modus befinden, bis die Daten des ersten Benutzers vom Server eintreffen. Somit wird jeweils nur ein Benutzer bearbeitet.

Dauert die Bearbeitung eines Benutzers beispielsweise etwa 300 ms, so ist es für alle Benutzer bereits eine Sekunde; der Zeitaufwand hängt linear von der Anzahl der Benutzer ab. Da die Ermittlung der Repo-Anzahl jedoch nicht voneinander abhängt, können die Prozesse parallelisiert werden. Dies erfordert die Arbeit mit .map und 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 empfängt eine Reihe von Versprechen als Eingabe und gibt ein Versprechen zurück. Letzteres ist abgeschlossen, nachdem alle Versprechen im Array abgeschlossen wurden oder bei der ersten Ablehnung. Es kann vorkommen, dass nicht alle gleichzeitig starten. Um einen gleichzeitigen Start zu gewährleisten, können Sie p-map verwenden.

Abschluss

Asynchrone Funktionen werden für die Entwicklung immer wichtiger. Nun, für die adaptive Nutzung asynchroner Funktionen sollten Sie Folgendes verwenden Asynchrone Iteratoren. Ein JavaScript-Entwickler sollte sich damit gut auskennen.

Skillbox empfiehlt:

Source: habr.com

Kommentar hinzufügen