Asynchrones JavaScript: Von Callback Hell zu Async and Await

Veröffentlicht: 2022-03-11

Einer der Schlüssel zum Schreiben einer erfolgreichen Webanwendung ist die Möglichkeit, Dutzende von AJAX-Aufrufen pro Seite durchzuführen.

Dies ist eine typische asynchrone Programmierherausforderung, und wie Sie mit asynchronen Aufrufen umgehen, wird zum großen Teil Ihre App und damit möglicherweise Ihr gesamtes Startup verändern.

Das Synchronisieren von asynchronen Aufgaben in JavaScript war lange Zeit ein ernstes Problem.

Diese Herausforderung betrifft Back-End-Entwickler, die Node.js verwenden, ebenso wie Front-End-Entwickler, die ein beliebiges JavaScript-Framework verwenden. Asynchrone Programmierung gehört zu unserem Arbeitsalltag, aber die Herausforderung wird oft auf die leichte Schulter genommen und nicht zum richtigen Zeitpunkt berücksichtigt.

Eine kurze Geschichte von asynchronem JavaScript

Die erste und unkomplizierteste Lösung waren verschachtelte Funktionen als Callbacks . Diese Lösung führte zu einer sogenannten Callback-Hölle , und zu viele Anwendungen leiden immer noch darunter.

Dann haben wir Promises bekommen. Dieses Muster machte den Code viel leichter lesbar, aber es war weit entfernt vom Don't Repeat Yourself (DRY)-Prinzip. Es gab immer noch zu viele Fälle, in denen Sie dieselben Codeteile wiederholen mussten, um den Fluss der Anwendung richtig zu verwalten. Die neueste Ergänzung in Form von async/await-Anweisungen machte asynchronen Code in JavaScript schließlich so einfach zu lesen und zu schreiben wie jeden anderen Code.

Werfen wir einen Blick auf die Beispiele für jede dieser Lösungen und reflektieren wir die Entwicklung der asynchronen Programmierung in JavaScript.

Dazu untersuchen wir eine einfache Aufgabe, die die folgenden Schritte ausführt:

  1. Überprüfen Sie den Benutzernamen und das Kennwort eines Benutzers.
  2. Rufen Sie Anwendungsrollen für den Benutzer ab.
  3. Anwendungszugriffszeit für den Benutzer protokollieren.

Ansatz 1: Callback Hell („The Pyramid of Doom“)

Die alte Lösung, um diese Aufrufe zu synchronisieren, war über verschachtelte Rückrufe. Dies war ein anständiger Ansatz für einfache asynchrone JavaScript-Aufgaben, konnte jedoch aufgrund eines Problems namens Callback Hell nicht skaliert werden.

Abbildung: Asynchrones JavaScript-Callback-Höllen-Antimuster

Der Code für die drei einfachen Aufgaben würde in etwa so aussehen:

 const verifyUser = function(username, password, callback){ dataBase.verifyUser(username, password, (error, userInfo) => { if (error) { callback(error) }else{ dataBase.getRoles(username, (error, roles) => { if (error){ callback(error) }else { dataBase.logAccess(username, (error) => { if (error){ callback(error); }else{ callback(null, userInfo, roles); } }) } }) } }) };

Jede Funktion erhält ein Argument, das eine andere Funktion ist, die mit einem Parameter aufgerufen wird, der die Antwort der vorherigen Aktion ist.

Zu viele Menschen werden Gehirnfrost erleben, nur wenn sie den obigen Satz lesen. Eine Anwendung mit Hunderten ähnlicher Codeblöcke wird der Person, die den Code verwaltet, noch mehr Ärger bereiten, selbst wenn sie ihn selbst geschrieben hat.

Dieses Beispiel wird noch komplizierter, wenn Sie feststellen, dass eine database.getRoles eine weitere Funktion mit verschachtelten Callbacks ist.

 const getRoles = function (username, callback){ database.connect((connection) => { connection.query('get roles sql', (result) => { callback(null, result); }) }); };

Neben schwer wartbarem Code hat das DRY-Prinzip in diesem Fall absolut keinen Wert. Beispielsweise wird die Fehlerbehandlung in jeder Funktion wiederholt und der Haupt-Callback wird von jeder verschachtelten Funktion aufgerufen.

Komplexere asynchrone JavaScript-Operationen, wie z. B. das Durchlaufen asynchroner Aufrufe, sind eine noch größere Herausforderung. Tatsächlich gibt es keine triviale Möglichkeit, dies mit Rückrufen zu tun. Aus diesem Grund haben JavaScript Promise-Bibliotheken wie Bluebird und Q so viel Anklang gefunden. Sie bieten eine Möglichkeit, allgemeine Vorgänge für asynchrone Anforderungen auszuführen, die die Sprache selbst noch nicht bereitstellt.

Hier kommen native JavaScript Promises ins Spiel.

JavaScript verspricht

Versprechen waren der nächste logische Schritt, um der Callback-Hölle zu entkommen. Diese Methode hat die Verwendung von Rückrufen nicht entfernt, aber sie hat die Verkettung von Funktionen unkompliziert gemacht und den Code vereinfacht, wodurch er viel einfacher zu lesen ist.

Abbildung: Diagramm der asynchronen JavaScript-Versprechen

Mit Promises würde der Code in unserem asynchronen JavaScript-Beispiel etwa so aussehen:

 const verifyUser = function(username, password) { database.verifyUser(username, password) .then(userInfo => dataBase.getRoles(userInfo)) .then(rolesInfo => dataBase.logAccess(rolesInfo)) .then(finalResult => { //do whatever the 'callback' would do }) .catch((err) => { //do whatever the error handler needs }); };

Um diese Einfachheit zu erreichen, müssten alle im Beispiel verwendeten Funktionen Promisified sein. Schauen wir uns an, wie die Methode getRoles aktualisiert würde, um ein Promise zurückzugeben:

 const getRoles = function (username){ return new Promise((resolve, reject) => { database.connect((connection) => { connection.query('get roles sql', (result) => { resolve(result); }) }); }); };

Wir haben die Methode geändert, um ein Promise mit zwei Rückrufen zurückzugeben, und das Promise selbst führt Aktionen von der Methode aus. Jetzt werden resolve und reject den Methoden Promise.then bzw. Promise.catch .

Sie werden vielleicht feststellen, dass die getRoles Methode intern immer noch anfällig für das Pyramid of Doom-Phänomen ist. Dies liegt an der Art und Weise, wie Datenbankmethoden erstellt werden, da sie Promise nicht zurückgeben. Wenn unsere Datenbankzugriffsmethoden auch Promise zurückgeben, würde die getRoles Methode wie folgt aussehen:

 const getRoles = new function (userInfo) { return new Promise((resolve, reject) => { database.connect() .then((connection) => connection.query('get roles sql')) .then((result) => resolve(result)) .catch(reject) }); };

Ansatz 3: Async/Warten

Die Pyramide des Schicksals wurde mit der Einführung von Promises erheblich gemildert. Allerdings mussten wir uns immer noch auf Callbacks verlassen, die an .then und .catch Methoden eines Promise übergeben werden.

Promises ebneten den Weg zu einer der coolsten Verbesserungen in JavaScript. ECMAScript 2017 brachte syntaktischen Zucker zusätzlich zu Promises in JavaScript in Form von async und await -Anweisungen.

Sie ermöglichen es uns, Promise -basierten Code so zu schreiben, als ob er synchron wäre, aber ohne den Haupt-Thread zu blockieren, wie dieses Codebeispiel zeigt:

 const verifyUser = async function(username, password){ try { const userInfo = await dataBase.verifyUser(username, password); const rolesInfo = await dataBase.getRoles(userInfo); const logStatus = await dataBase.logAccess(userInfo); return userInfo; }catch (e){ //handle errors as needed } };

Awaiting Promise to resolve ist nur innerhalb von async -Funktionen zulässig, was bedeutet, dass verifyUser mit async function definiert werden musste.

Sobald diese kleine Änderung jedoch vorgenommen wurde, können Sie auf jedes Promise await , ohne zusätzliche Änderungen an anderen Methoden vorzunehmen.

Async - Eine lang erwartete Auflösung eines Versprechens

Asynchrone Funktionen sind der nächste logische Schritt in der Entwicklung der asynchronen Programmierung in JavaScript. Sie machen Ihren Code viel sauberer und einfacher zu warten. Wenn Sie eine Funktion als async , wird sichergestellt, dass sie immer ein Promise zurückgibt, sodass Sie sich darüber keine Gedanken mehr machen müssen.

Warum sollten Sie heute mit der Verwendung der JavaScript async Funktion beginnen?

  1. Der resultierende Code ist viel sauberer.
  2. Die Fehlerbehandlung ist viel einfacher und basiert auf try / catch genau wie in jedem anderen synchronen Code.
  3. Das Debuggen ist viel einfacher. Das Festlegen eines Haltepunkts innerhalb eines .then Blocks wird nicht zum nächsten .then -Block verschoben, da er nur den synchronen Code schrittweise durchläuft. Sie können await jedoch durchlaufen, als wären es synchrone Anrufe.