
Der Autor des Artikels analysiert Async / Await in JavaScript anhand von Beispielen. Im Allgemeinen ist Async / Await eine bequeme Möglichkeit, asynchronen Code zu schreiben. Vor dieser Gelegenheit wurde ein ähnlicher Code mit Rückrufen und Versprechungen geschrieben. Der Autor des Originalartikels zeigt anhand verschiedener Beispiele die Vorteile von Async / Await auf.
Wir erinnern Sie daran: Für alle Leser von „Habr“ - ein Rabatt von 10.000 Rubel bei der Anmeldung für einen Skillbox-Kurs mit dem Promo-Code „Habr“.
Skillbox empfiehlt: Der Java Developer Online Education-Kurs.
Rückruf
Rückruf ist eine Funktion, deren Aufruf auf unbestimmte Zeit verzögert wird. Bisher wurden Rückrufe in den Teilen des Codes verwendet, in denen das Ergebnis nicht sofort abgerufen werden konnte.
Hier ist ein Beispiel für das asynchrone Lesen einer Datei auf Node.js:
fs.readFile(__filename, 'utf-8', (err, data) => { if (err) { throw err; } console.log(data); });
Probleme treten auf, wenn Sie mehrere asynchrone Vorgänge gleichzeitig ausführen 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.
Konvertieren Sie das Bild nach dem Herunterladen in ein anderes Format, z. B. von PNG nach JPEG. Wenn die Konvertierung erfolgreich war, wird eine E-Mail an die E-Mail des Benutzers gesendet. Außerdem werden Informationen zum Ereignis mit dem Datum in die Datei transformations.log eingegeben.

Es lohnt sich, im letzten Teil des Codes auf das Auferlegen von Rückrufen und eine große Anzahl}) zu achten. Dies wird als Callback Hell oder Pyramid of Doom bezeichnet.
Die Nachteile dieser Methode liegen auf der Hand:
- Dieser Code ist schwer zu lesen.
- Es ist auch schwierig, mit Fehlern umzugehen, was häufig zu einer Verschlechterung der Codequalität führt.
Um dieses Problem zu lösen, wurden JavaScript Versprechen hinzugefügt. Mit ihnen können Sie die tiefe Verschachtelung von Rückrufen durch das Wort .then ersetzen.

Der positive Punkt der Versprechen war, dass bei ihnen der Code viel besser gelesen wird, von oben nach unten und nicht von links nach rechts. Versprechen haben aber auch ihre Probleme:
- Müssen Sie eine große Menge von .then hinzufügen.
- Anstelle von try / catch wird .catch verwendet, um alle Fehler zu behandeln.
- Das Arbeiten mit mehreren Versprechungen innerhalb eines Zyklus ist bei weitem nicht immer bequem, in einigen Fällen erschweren sie den Code.
Hier ist eine Aufgabe, die die Bedeutung des letzten Absatzes zeigt.
Angenommen, es gibt eine for-Schleife, die eine Folge von Zahlen von 0 bis 10 mit einem zufälligen Intervall (0 - n Sekunden) druckt. Mithilfe von Versprechungen müssen Sie diesen Zyklus so ändern, dass die Zahlen in der Reihenfolge von 0 bis 10 angezeigt werden. Wenn also die Nullausgabe 6 Sekunden und die Einheiten 2 Sekunden dauert, muss zuerst Null ausgegeben werden, und dann beginnt der Countdown für die Einheitenausgabe.
Um dieses Problem zu lösen, verwenden wir natürlich weder Async / Await noch .sort. Ein Beispiel für eine Lösung ist am Ende.
Asynchrone Funktionen
Das Hinzufügen von asynchronen Funktionen zu ES2017 (ES8) hat die Arbeit mit Versprechungen vereinfacht. Ich stelle fest, dass asynchrone Funktionen zusätzlich zu Versprechungen funktionieren. Diese Funktionen repräsentieren keine qualitativ unterschiedlichen Konzepte. Asynchrone Funktionen wurden als Alternative zu Code konzipiert, der Versprechen verwendet.
Async / Await ermöglicht es, die Arbeit mit asynchronem Code synchron zu organisieren.
Die Kenntnis von Versprechungen erleichtert somit das Verständnis der Prinzipien von Async / Await.
SyntaxIn einer typischen Situation besteht es aus zwei Schlüsselwörtern: asynchron und warten. Das erste Wort macht die Funktion asynchron. Diese Funktionen ermöglichen das Warten. In jedem anderen Fall führt die Verwendung dieser Funktion zu einem Fehler.
Async wird ganz am Anfang der Funktionsdeklaration und bei der Pfeilfunktion zwischen dem Zeichen "=" und den Klammern eingefügt.
Diese Funktionen können als Methoden in ein Objekt eingefügt oder in einer Klassendeklaration verwendet werden.
NB! Es ist zu beachten, dass Klassenkonstruktoren und Getter / Setter nicht asynchron sein können.
Semantik und AusführungsregelnAsync-Funktionen ähneln im Wesentlichen Standard-JS-Funktionen, es gibt jedoch Ausnahmen.
Asynchrone Funktionen geben also immer Versprechen zurück:
async function fn() { return 'hello'; } fn().then(console.log)
Insbesondere gibt fn den String hallo zurück. Da dies eine asynchrone Funktion ist, wird der Zeichenfolgenwert mithilfe des Konstruktors in ein Versprechen eingeschlossen.
Hier ist ein alternatives Design ohne Async:
function fn() { return Promise.resolve('hello'); } fn().then(console.log);
In diesem Fall erfolgt die Rückgabe des Versprechens "manuell". Eine asynchrone Funktion hüllt sich immer in ein neues Versprechen.
Für den Fall, dass der Rückgabewert ein Grundelement ist, gibt die asynchrone Funktion einen Wert zurück und verpackt ihn in ein Versprechen. Für den Fall, dass der Rückgabewert Gegenstand des Versprechens ist, wird seine Lösung im neuen Versprechen zurückgegeben.
const p = Promise.resolve('hello') p instanceof Promise;
Aber was passiert, wenn innerhalb der 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 einem Redject zurück. In dieser Situation gibt Promise.reject anstelle von Promise.resolve einen Fehler zurück.
Asynchrone Funktionen bei der Ausgabe geben immer Versprechen, unabhängig davon, was zurückgegeben wird.
Asynchrone Funktionen werden bei jedem Warten angehalten.
Warten wirkt sich auf Ausdrücke aus. Wenn der Ausdruck ein Versprechen ist, wird die asynchrone Funktion ausgesetzt, bis das Versprechen ausgeführt wird. Falls der Ausdruck kein Versprechen ist, wird er über Promise.resolve in ein Versprechen umgewandelt und dann beendet.
Hier finden Sie eine Beschreibung der Funktionsweise der Funktion fn.
- Nach dem Aufruf wird die erste Zeile von const a = await 9 konvertiert; in const a = warte auf Promise.resolve (9);
- Nach der Verwendung von Await wird die Ausführung der Funktion angehalten, bis sie ihren Wert erhält (in der aktuellen Situation sind es 9).
- delayAndGetRandom (1000) unterbricht die Ausführung der Funktion fn, bis sie sich selbst beendet (nach 1 Sekunde). Dadurch wird die fn-Funktion tatsächlich für 1 Sekunde gestoppt.
- delayAndGetRandom (1000) durch Auflösung gibt einen zufälligen Wert zurück, der dann der Variablen b zugewiesen wird.
- Nun, der Fall der Variablen c ähnelt dem Fall der Variablen a. Danach stoppt alles für eine Sekunde, aber jetzt gibt delayAndGetRandom (1000) nichts zurück, da dies nicht erforderlich ist.
- Infolgedessen werden die Werte nach der Formel a + b * c berechnet. Das Ergebnis wird mit Promise.resolve in ein Versprechen eingeschlossen und von der Funktion zurückgegeben.
Diese Pausen ähneln möglicherweise Generatoren in ES6, es gibt jedoch
Gründe dafür .
Wir lösen das Problem
Nun schauen wir uns die Lösung für das oben erwähnte Problem an.

Die Funktion finishMyTask verwendet Await, um auf die Ergebnisse von Vorgängen wie queryDatabase, sendEmail, logTaskInFile und anderen zu warten. Wenn wir diese Entscheidung mit dem Ort vergleichen, an dem die Versprechen verwendet wurden, werden die Ähnlichkeiten offensichtlich. Trotzdem vereinfacht die Version mit Async / Await alle syntaktischen Schwierigkeiten erheblich. In diesem Fall gibt es nicht viele Rückrufe 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));
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); } }
FehlerbehandlungUnverarbeitete Fehler werden in abgelehnte Versprechen eingewickelt. In asynchronen Funktionen können Sie jedoch das try / catch-Konstrukt verwenden, um eine synchrone Fehlerbehandlung durchzuführen.
async function canRejectOrReturn() {
canRejectOrReturn () ist eine asynchrone Funktion, die entweder erfolgreich ist („perfekte Zahl“) oder mit einem Fehler fehlschlägt („Entschuldigung, Zahl zu groß“).
async function foo() { try { await canRejectOrReturn(); } catch (e) { return 'error caught'; } }
Da erwartet wird, dass canRejectOrReturn im obigen Beispiel ausgeführt wird, führt seine eigene erfolglose Beendigung zur Ausführung des catch-Blocks. Infolgedessen endet die foo-Funktion entweder mit undefined (wenn im try-Block nichts zurückgegeben wird) oder mit einem abgefangenen Fehler. Infolgedessen schlägt diese Funktion nicht fehl, da try / catch die foo-Funktion selbst übernimmt.
Hier ist ein weiteres Beispiel:
async function foo() { try { return canRejectOrReturn(); } catch (e) { return 'error caught'; } }
Es ist zu beachten, dass im Beispiel von foo canRejectOrReturn zurückgegeben wird. Foo wird in diesem Fall entweder mit einer perfekten Zahl abgeschlossen oder es wird ein Fehlerfehler ("Entschuldigung, Nummer zu groß") zurückgegeben. Der catch-Block wird niemals ausgeführt.
Das Problem ist, dass foo das von canRejectOrReturn übergebene Versprechen zurückgibt. Daher wird die Lösung für die Funktion foo zur Lösung für canRejectOrReturn. In diesem Fall besteht der Code nur aus zwei Zeilen:
try { const promise = canRejectOrReturn(); return promise; }
Aber was passiert, wenn Sie warten und zusammen zurückkehren:
async function foo() { try { return await canRejectOrReturn(); } catch (e) { return 'error caught'; } }
Im obigen Code ist foo erfolgreich, wenn sowohl die perfekte Nummer als auch der Fehler abgefangen wurden. Es wird keine Fehler geben. Aber foo endet mit canRejectOrReturn und nicht mit undefined. Stellen Sie dies sicher, indem Sie die Zeile return wait canRejectOrReturn () 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.
Vergessen wartenDies passiert ziemlich oft - vor dem Versprechen wird das Schlüsselwort await vergessen:
async function foo() { try { canRejectOrReturn(); } catch (e) { return 'caught'; } }
Wie Sie sehen, gibt es im Code weder Warten noch Zurück. Daher wird foo immer mit einer undefinierten Verzögerung von 1 Sekunde beendet. Aber das Versprechen wird erfüllt. Wenn es einen Fehler oder ein Redject gibt, wird UnhandledPromiseRejectionWarning aufgerufen.
Asynchrone Funktionen in RückrufenAsynchrone Funktionen werden häufig in .map oder .filter als Rückrufe verwendet. Ein Beispiel ist die Funktion fetchPublicReposCount (Benutzername), die die Anzahl der auf GitHub geöffneten Repositorys zurückgibt. Angenommen, es gibt drei Benutzer, deren Metriken wir benötigen. Hier ist der Code für diese Aufgabe:
const url = 'https://api.github.com/users';
Wir brauchen Konten ArfatSalman, Octocat, Norvig. Führen Sie in diesem Fall Folgendes aus:
const users = [ 'ArfatSalman', 'octocat', 'norvig' ]; const counts = users.map(async username => { const count = await fetchPublicReposCount(username); return count; });
Sie sollten im .map-Rückruf auf Await achten. Hier zählt eine Reihe von Versprechungen. Nun, .map ist ein anonymer Rückruf für jeden angegebenen Benutzer.
Übermäßig konsequente Nutzung von WartenNehmen Sie den folgenden 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 Nummer dem Zählarray hinzugefügt. Das Problem mit dem Code besteht darin, dass sich alle nachfolgenden Benutzer im Standby-Modus befinden, bis die ersten Benutzerdaten vom Server eingehen. Somit wird in einem einzigen Moment nur ein Benutzer verarbeitet.
Wenn beispielsweise die Verarbeitung eines Benutzers etwa 300 ms dauert, ist dies für alle Benutzer bereits eine Sekunde. Die linear verbrachte Zeit hängt von der Anzahl der Benutzer ab. Da das Abrufen der Anzahl der Repos 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 am Eingang erhält mit der Rückgabe des Versprechens eine Reihe von Versprechungen. Der letzte nach Abschluss aller Versprechen im Array oder beim ersten Redject ist abgeschlossen. Es kann vorkommen, dass nicht alle gleichzeitig gestartet werden. Um einen gleichzeitigen Start zu gewährleisten, können Sie p-map verwenden.
Fazit
Asynchrone Funktionen werden für die Entwicklung immer wichtiger. Für die adaptive Verwendung von Async-Funktionen lohnt es sich,
Async-Iteratoren zu verwenden . Der JavaScript-Entwickler sollte sich damit auskennen.
Skillbox empfiehlt: