Ein Leitfaden für Anfänger zu asynchronem JavaScript
Aktualisiert auf November 15, 2024 10 Minuten gelesen

Wenn Sie gerade erst mit dem Programmieren beginnen, ist die Wahrscheinlichkeit groß, dass Sie sich Programme als einen Satz aufeinanderfolgender Logikblöcke vorstellen, bei denen jeder Block eine bestimmte Aufgabe ausführt und sein Ergebnis weitergibt, damit der nächste Block ausgeführt werden kann usw. und so weiter Meistens haben Sie recht, die meisten Programme laufen sequentiell ab. Dieses Modell ermöglicht es uns, Programme zu erstellen, die einfach zu schreiben und zu warten sind. Es gibt jedoch bestimmte Anwendungsfälle, in denen dieses sequentielle Modell nicht funktionieren würde oder nicht optimal wäre. Betrachten Sie als Beispiel eine Buchleseanwendung. Diese Anwendung verfügt über einige erweiterte Funktionen, z. B. das Finden aller Vorkommen eines Wortes, das Navigieren zwischen Lesezeichen und ähnliches. Stellen Sie sich nun vor, der Benutzer liest gerade ein langes Buch und beschließt, nach allen Vorkommen eines gebräuchlichen Wortes wie „The“ zu suchen. Die Anwendung benötigt normalerweise einige Sekunden, um alle Vorkommen dieses Worts zu finden und zu indizieren. In einem sequentiellen Programm kann der Benutzer nicht mit der Anwendung interagieren (die Seite ändern oder einen Text hervorheben), bis der Suchvorgang abgeschlossen ist. Hoffentlich sehen Sie, dass das keine optimale Benutzererfahrung ist!
Das Diagramm veranschaulicht einen typischen Ausführungsablauf der Buchleseanwendung. Wenn der Benutzer einen lang andauernden Vorgang initiiert (in diesem Fall die Suche nach allen Vorkommen von „the“ in einem großen Buch), „friert“ die Anwendung für die gesamte Dauer dieses Vorgangs ein. In diesem Fall klickt der Benutzer so lange ohne Ergebnis auf die Schaltfläche „Nächstes Lesezeichen“, bis der Suchvorgang abgeschlossen ist und alle Vorgänge sofort wirksam werden, was dem Endbenutzer das Gefühl einer verzögerten Anwendung vermittelt.
Möglicherweise ist Ihnen aufgefallen, dass dieses Beispiel nicht wirklich dem sequenziellen Modell entspricht, das wir zuvor vorgestellt haben. Dies liegt daran, dass die Operationen hier unabhängig voneinander sind. Der Benutzer muss nicht wissen, wie oft „the“ vorkommt, um zum nächsten Lesezeichen zu navigieren, daher ist die Reihenfolge der Ausführung von Vorgängen nicht wirklich wichtig. Wir müssen nicht auf das Ende des Suchvorgangs warten, bevor wir zum nächsten Lesezeichen navigieren können. Eine mögliche Verbesserung des vorherigen Ausführungsablaufs basiert auf dieser Logik: Wir können den langen Suchvorgang im Hintergrund ausführen, mit allen eingehenden Vorgängen fortfahren und sobald der lange Vorgang abgeschlossen ist, können wir den Benutzer einfach benachrichtigen. Der Ausführungsablauf sieht wie folgt aus:
Mit diesem Ausführungsablauf wird die Benutzererfahrung deutlich verbessert. Jetzt kann der Benutzer einen lang andauernden Vorgang starten, die Anwendung normal weiter verwenden und wird benachrichtigt, sobald der Vorgang abgeschlossen ist. Dies ist die Grundlage der asynchronen Programmierung.
Javascript unterstützt neben anderen Sprachen diesen Stil der asynchronen Programmierung, indem es umfangreiche APIs bereitstellt, um nahezu jedes erdenkliche asynchrone Verhalten zu erreichen. Letztendlich sollte Javascript von Natur aus eine asynchrone Sprache sein. Wenn wir uns auf das vorherige Beispiel beziehen, ist die asynchrone Logik die Grundlage aller Benutzerinteraktionsanwendungen, und Javascript wurde in erster Linie für die Verwendung im Browser entwickelt, wo es bei den meisten Programmen darum geht, auf Benutzeraktionen zu reagieren.
Im Folgenden finden Sie eine kurze Anleitung zu asynchronem Javascript:
Rückrufe
In einem typischen Programm finden Sie normalerweise eine Reihe von Funktionen. Um eine Funktion zu verwenden, rufen wir sie mit einer Reihe von Parametern auf. Der Funktionscode wird ausgeführt und gibt ein Ergebnis zurück, nichts Außergewöhnliches. Die asynchrone Programmierung verschiebt diese Logik geringfügig. Um auf das Beispiel der Buchleseanwendung zurückzukommen: Wir können keine reguläre Funktion zum Implementieren der Suchoperationslogik verwenden, da die Operation eine unbekannte Zeitdauer in Anspruch nimmt. Eine reguläre Funktion kehrt grundsätzlich zurück, bevor die Operation abgeschlossen ist, und dies ist nicht das erwartete Verhalten. Die Lösung besteht darin, eine andere Funktion anzugeben, die ausgeführt wird, sobald der Suchvorgang abgeschlossen ist. Dies modelliert unseren Anwendungsfall, da unser Programm seinen Ablauf normal fortsetzen kann und sobald der Suchvorgang abgeschlossen ist, die angegebene Funktion ausgeführt wird, um den Benutzer über die Suchergebnisse zu informieren. Diese Funktion nennen wir eine Callback-Funktion:
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
Zuerst definieren wir die Suchoperationsfunktion searchOccurrences. Es benötigt das zu suchende Wort und einen zweiten Parameter „Callback“, der die Funktion darstellt, die ausgeführt wird, sobald der Suchvorgang abgeschlossen ist. Die Suchoperationsfunktion wurde bewusst abstrakt gehalten, wir müssen uns nur auf ihre zwei möglichen Ergebnisse konzentrieren: Im ersten Fall ist alles erfolgreich verlaufen und wir haben das Ergebnis der Suche in der Ergebnisvariablen. In diesem Fall müssen wir nur die Callback-Funktion mit den folgenden Parametern aufrufen: Der erste Parameter ist null, was bedeutet, dass kein Fehler aufgetreten ist, der zweite Parameter ist das gesuchte Wort und der dritte und vielleicht wichtigste der drei Parameter ist das Ergebnis des Suchvorgangs.
Im zweiten Fall tritt ein Fehler auf. Dies ist auch ein Fall, in dem die Ausführung der Suchoperation abgeschlossen ist und wir die Rückruffunktion aufrufen müssen. Wir verwenden einen Try-and-Catch-Block, um jeden Fehler abzufangen, und rufen einfach die Callback-Funktion mit dem Fehlerobjekt aus dem Catch-Block auf.
Anschließend haben wir die Rückruffunktion handleSearchOccurrences definiert und ihre Logik recht einfach gehalten. Es geht lediglich darum, eine Nachricht an die Konsole zu drucken. Wir überprüfen zunächst den Parameter „err“, um festzustellen, ob in der Hauptfunktion ein Fehler aufgetreten ist. In diesem Fall teilen wir dem Benutzer lediglich mit, dass der Suchvorgang mit einem Fehler beendet wurde. Wenn keine Fehler aufgetreten sind, drucken wir eine Meldung mit dem Ergebnis des Suchvorgangs.
Abschließend rufen wir die Funktion „searchOccurrences“ mit dem Wort „the“ auf. Die Funktion wird nun normal ausgeführt, ohne das Hauptprogramm zu blockieren. Sobald die Suche abgeschlossen ist, wird der Rückruf ausgeführt und wir erhalten die Ergebnismeldung entweder mit dem Suchergebnis oder der Fehlermeldung.
An dieser Stelle ist es wichtig zu erwähnen, dass wir nur innerhalb der Haupt- und Rückruffunktionen Zugriff auf die Ergebnisvariable haben. Wenn wir so etwas versuchen:
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
Das Ergebnis des Drucks wäre undefiniert, da die Programme nicht auf die Ausführung der Funktion „searchOccurrences“ warten. Es geht zur nächsten Anweisung über, bei der es sich um die Druckanweisung handelt, bevor die Ergebnisvariable innerhalb der Hauptfunktion zugewiesen wird. Als Ergebnis drucken wir die nicht zugewiesene Ergebnisvariable.
Basierend auf dieser Logik sollten wir also den gesamten Code, der die Ergebnisvariable verwendet, innerhalb der Rückruffunktion behalten. Dies scheint jetzt vielleicht kein Problem zu sein, könnte sich aber schnell zu einem echten Problem entwickeln. Stellen Sie sich den Fall vor, dass wir eine Kette asynchroner Funktionen haben, die nacheinander ausgeführt werden müssen. In der typischen Rückruflogik würden Sie etwa Folgendes implementieren:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Beachten Sie, dass jeder Rückruf einen Fehlerparameter hat und jeder Fehler separat behandelt werden muss. Dies macht den oben bereits komplexen Code noch komplexer und schwieriger zu warten. Hoffentlich sind Promises hier, um das Callback-Höllenproblem zu lösen, wir werden uns als nächstes damit befassen.
Versprechen
Versprechen basieren auf Rückrufen und funktionieren auf ähnliche Weise. Sie wurden als Teil der ES6-Funktionen eingeführt, um einige der eklatanten Probleme mit Rückrufen wie Callback Hell zu lösen. Versprechen stellen ihre eigenen Funktionen bereit, die bei erfolgreichem Abschluss (Auflösen) und beim Auftreten von Fehlern (Ablehnen) ausgeführt werden. Im Folgenden wird das mit Versprechen implementierte Beispiel für searchOccurrences gezeigt:
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Sehen wir uns die Änderungen an, die wir vorgenommen haben:
Die Funktion „searchOccurrences“ gibt ein Versprechen zurück. Innerhalb des Versprechens behalten wir die gleiche Logik bei: Wir haben zwei Funktionen „resolve“ und „reject“, die unsere Rückrufe darstellen, anstatt eine einzelne Rückruffunktion, die sowohl eine erfolgreiche Ausführung als auch eine Ausführung mit Fehlern verarbeitet. Versprechen trennen die beiden Ergebnisse und sorgen für eine saubere Syntax beim Aufruf der Hauptfunktion. Die Auflösungsfunktion wird mit dem Schlüsselwort „then“ an die Hauptfunktion „eingehängt“. Hier geben wir einfach die beiden Parameter der Auflösungsfunktion an und drucken das Suchergebnis aus. Ähnliches gilt für die Reject-Funktion, sie kann mit dem Schlüsselwort „catch“ eingebunden werden. Hoffentlich können Sie die versprochenen Vorteile in Bezug auf Lesbarkeit und Sauberkeit des Codes zu schätzen wissen. Wenn Sie immer noch darüber diskutieren, schauen Sie sich an, wie wir das Callback-Höllenproblem lösen können, indem wir die asynchronen Funktionen so verketten, dass sie eine nach der anderen ausgeführt werden:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Async/Warten
Async/Await sind die neueste Ergänzung zu unserem asynchronen Toolbelt in Javascript. Sie wurden mit ES8 eingeführt und bieten eine neue Abstraktionsebene zusätzlich zu asynchronen Funktionen, indem sie einfach auf die Ausführung einer asynchronen Operation „warten“. Der Programmablauf blockiert bei dieser Anweisung, bis ein Ergebnis von der asynchronen Operation zurückgegeben wird, und dann fährt das Programm mit der nächsten Anweisung fort. Wenn Sie über den synchronen Ausführungsablauf nachdenken, haben Sie Recht. Für uns schließt sich der Kreis! Async/await versucht, die Einfachheit der synchronen Programmierung in die asynchrone Welt zu bringen. Bitte beachten Sie, dass dies nur in der Ausführung und im Code des Programms spürbar ist. Unter der Haube bleibt alles beim Alten, Async/await verwendet immer noch Versprechen und Rückrufe sind ihre Bausteine.
Sehen wir uns unser Beispiel an und implementieren es mit Async/await:
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
An unserem Code hat sich nicht viel geändert. Wichtig ist hier das Schlüsselwort „async“ vor der Funktionsdeklaration „searchOccurrences“. Dies zeigt an, dass die Funktion asynchron ist. Beachten Sie außerdem das Schlüsselwort „await“, wenn Sie die Funktion „searchOccurrences“ aufrufen. Dadurch wird das Programm angewiesen, auf die Ausführung der Funktion zu warten, bis das Ergebnis zurückgegeben wird, bevor das Programm mit der nächsten Anweisung fortfahren kann. Mit anderen Worten: Die Ergebnisvariable enthält immer den zurückgegebenen Wert der Funktion „searchOccurrences“ und nicht das Versprechen von Die Funktion Async/Await hat in diesem Sinne keinen ausstehenden Status als Promises. Sobald die Ausführung abgeschlossen ist, gehen wir zur print-Anweisung über und dieses Mal enthält das Ergebnis tatsächlich das Ergebnis des Suchvorgangs. Wie erwartet verhält sich der neue Code genauso, als wäre er synchron.
Eine weitere Kleinigkeit, die Sie beachten sollten, ist, dass wir den Fehler „searchOccurrences“ innerhalb derselben Funktion behandeln müssen, da wir keine Rückruffunktionen mehr haben, da wir den Fehler nicht einfach an die Rückruffunktion weitergeben und dort behandeln können. Hier geben wir nur als Beispiel eine Fehlermeldung aus, falls ein Fehler auftritt.
Zusammenfassung
In diesem Artikel haben wir die verschiedenen Ansätze zur Implementierung asynchroner Logik in Javascript erläutert. Wir begannen damit, ein konkretes Beispiel dafür zu untersuchen, warum wir vom regulären synchronen Programmierstil zum asynchronen Modell wechseln müssten. Anschließend sind wir zu Callbacks übergegangen, den Hauptbausteinen von asynchronem Javascript. Die Einschränkungen von Rückrufen führten uns zu den verschiedenen Alternativen, die im Laufe der Jahre hinzugefügt wurden, um diese Einschränkungen zu überwinden, hauptsächlich Versprechen und Async/await. Asynchrone Logik ist überall im Web zu finden, egal ob Sie eine externe API aufrufen, eine Datenbankabfrage initiieren, in das lokale Dateisystem schreiben oder sogar auf Benutzereingaben in einem Anmeldeformular warten. Hoffentlich fühlen Sie sich jetzt sicherer, diese Probleme anzugehen, indem Sie sauberes und wartbares asynchrones Javascript schreiben!
Wenn Ihnen dieser Artikel gefällt, schauen Sie sich bitte den CLA-Blog an, in dem wir verschiedene Themen zum Einstieg in die Technik besprechen. Schauen Sie sich auch unseren YouTube-Kanal für unsere früheren kostenlosen Workshops an und folgen Sie uns auf sozialen Medien, damit Sie die kommenden nicht verpassen!
Machen Sie Ihre Karriere zukunftssicher, indem Sie sich im Web Development Bootcamp von Code Labs Academy in HTML, CSS und JavaScript weiterbilden.