Asynchrones JavaScript: Von Callback Hell zu Async and Await
Veröffentlicht: 2022-03-11Einer 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:
- Überprüfen Sie den Benutzernamen und das Kennwort eines Benutzers.
- Rufen Sie Anwendungsrollen für den Benutzer ab.
- 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.
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.

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?
- Der resultierende Code ist viel sauberer.
- Die Fehlerbehandlung ist viel einfacher und basiert auf
try
/catch
genau wie in jedem anderen synchronen Code. - 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önnenawait
jedoch durchlaufen, als wären es synchrone Anrufe.