Verarbeitung synchron / asynchron: nacheinander oder Ausführung im Hintergrund
JavaScript arbeitet von Haus aus »synchron«: Der Code wird Anweisung für Anweisung ausgeführt und jede Aktion, jede Berechnung, jedes Laden einer Datei blockiert den nächsten Schritt. Einige JavaScript-Funktionen können die synchrone Natur außer Kraft setzen: setTimeout / setInterval, das Warten auf ein Event mit addEventListener, fetch und der asynchrone XMLHttpRequest.
Synchrone Verarbeitung vs asynchrone Methoden
In einem einfachen synchronen Programmablauf arbeitet Javascript Anweisungen Zeile für Zeile ab. Zeile 3 muss ausgeführt werden, bevor Zeile 4 in Angriff genommen wird.
Wenn eine Aufgabe – z.B. eine Berechnung oder das Einlesen einer Datei – länger dauert, blockiert ihre Ausführung alle folgenden Aktionen.
Diese Form der Verarbeitung kann man sich vorstellen wie ein Pizzabäcker mit einem kleinen Pizzaofen, in den immer nur eine Pizza passt: Erst wenn die Pizza fertig gebacken ist, kann der Pizzabäcker die nächste Pizza in den Ofen schieben.
Asynchrone Funktionen
Asynchrone Funktionen werden »quasi parallel« zu den folgenden Anweisungen durchgeführt – aber nur quasi, denn Javascript kann nur eine Anweisung nach der anderen durchführen.
In der Analogie des Pizzabäckers ist Platz für mehrere Pizzas und wann immer eine Pizza fertig ist, kann der Bäcker sie herausholen und eine neue Pizza nachschieben, wenn Bestellungen vorliegen.
Tatsächlich warten asynchrone Funktionen in einer Warteschlange auf Events und werden beim Eintreten des Events in einem freien Moment ausgeführt.
Warten in der Task Queue
Wenn eine Aktion – z.B. das Lesen einer Datei auf dem Server – zu einer langatmigen Berechnung oder zum Warten auf Daten vom Server führt, und die Datei anfangs für die Darstellung der Seite nicht erforderlich ist, kann die Aktion aus der synchronen Verarbeitung herausgenommen werden – z.B. bis der Benutzer eine Aktion durchführt oder die Daten vom Server vorliegen.
console.log ("Schritt ⑤");
setTimeout (function () {
console.log ("Asynchroner Schritt ⑥");
}, 5000);
console.log ("Schritt ⑦");
Schritt ⑤ wird sofort ausgeführt. In der Browser-Console wird sichtbar, dass Schritt ⑦ von Schritt ⑥ ausgeführt wird.
[Log] Schritt 5 [Log] Schritt 7 [Log] Asynchroner Schritt 6
Der Call Stack
JavaScript führt viele Funktionen asynchron aus – besser: läßt sie ausführen, denn diese Aufgaben liegen nicht im nativen Javascript, sondern gehören zu den sogenannten Web Application Programming Interface. Asynchrone Funktionen lauern sozusagen in einem Hinterzimmer, das »Call Stack« genannt wird, und werden vom Browser zu gegebner Zeit zur Ausführung gebracht. Dazu gehören z.B.
- fetch,
- setTimeout,
- requestAnimationFrame,
- addEventListener,
- der XMLHTTPRequest.
Für die Behandlung asynchroner Aufgaben stellt Javascript mehrere Optionen zur Verfügung:
- Callback-Funktionen
- Promise
- Async / Await
Callback-Funktionen waren der Klassiker für asynchrone Abläufe in Javascript bis Promise und async/await standardisiert wurden. Für Aufrufe von fetch oder komplexe Abläufe sind Promises oder async/await einfacher und besser lesbar.
Asynchrone Verarbeitung mit Callback-Funktionen
Callbacks waren die ursprüngliche Methode zur Behandlung asynchroner Funktionen in JavaScript, bevor Promise und async/await standardisiert waren. Ein Callback ist einfach eine Funktion, die einer anderen Funktion übergeben wird, damit sie nach Abschluss der asynchronen Arbeit aufgerufen wird.
function ladeDatei(dateiname, callback) {
setTimeout(() => {
const daten = "Dateiinhalt von " + dateiname;
callback(daten);
}, 1000);
}
ladeDatei("test.txt", (inhalt) => {
console.log("Datei geladen:", inhalt);
});
setTimeout simuliert hier einen asynchronen Vorgang. callback wird aufgerufen, wenn die Arbeit fertig ist. Am besten vertraut ist das Prinzip des Callbacks im Event-Listener:
button.addEventListener("click", () => {
console.log("Button geklickt");
});
Callbacks eignen sich für kleine, einmalige asynchrone Tasks, aber bei verschachtelten asynchronen Operationen entsteht schnell eine »Callback-Hölle« – der Code wird schwer lesbar. Man stelle sich nur mal vor, eine Datei kann erst nach einem erfolgreichen Laden einer initialen Datei geladen werden.
ladeDatei("a.txt", (a) => {
ladeDatei("b.txt", (b) => {
ladeDatei("c.txt", (c) => {
console.log(a, b, c);
});
});
});
Javascript Promise
So weit, so gut. Aber was passiert, wenn das Bild nicht geladen werden kann? Oder wenn nicht ein Bild, sondern gleich mehrere Bilder geladen werden?
Das kommt oft vor: In Abhängigkeit vom Erfolg eines ersten Events soll ein weiteres Event behandelt werden. Hier setzt das Javascript Promise ein: Die Funktion wird in ein Versprechen (wenn … dann … sonst) gepackt.
new Promise (function (resolve, reject) {
let img = document.createElement('img');
img.src= 'image.jpg';
img.onload = resolve();
img.onerror = reject();
})
.then ( loadingSuccess () )
.catch ( loadingFailed () );
function loadingSuccess() {
console.log ("Bild geladen.");
}
function loadingFailed () {
console.log ("Bild konnte nicht geladen werden. Mach was anderes.");
}
Async / Await
await ist ein Operator, der dafür sorgt, dass der Aufruf abwartet, ob ein Promise erfüllt (resolve) oder abgelehnt (reject) wurde. Dieser Operator kann nur innerhalb von asynchronen Funktionen verwendet werden.
async als Teil der Deklaration einer Funktion gibt an, dass der Code asynchron ausgeführt werden soll. await vor einer Anweisung gibt ein Promise zurück, dass die Funktion anhält und auf das Ergebnis wartet, bevor die Ausführung fortgesetzt wird.
const getBooks = async (num) => {
try {
let response = await fetch ("file.json"); // fetch ist asynchron
let json = await response.json (); // aber wartet dank await auf die Antwort
let list = json.list;
console.log (list[num].autor); // Dritter Autor in der Liste
} catch (e) {
console.log ("Daten wurde nicht geladen", e);
}
}
getBooks (2);
Oder ein Bild laden, Breite und Höhe abfragen und dann ein img-Element einsetzen.
Die Methode HTMLImageElement.decode() entspricht einem onload mit Promise (gefunden auf async await in image loading auf Stackoverflow).
(async () => {
const img = new Image();
img.src = "spices.jpg";
await img.decode();
// genug gewartet, Bild geladen!
console.log( "width: " + img.naturalWidth + " height: " + img.naturalHeight );
document.querySelector("#viewbox").innerHTML = "<img src='" + img.src + "' width='" + "img.naturalWidth'" + " height='" + img.naturalHeight + "'>";
})();
Asynchrone Ereignisse
Javascript ist »Single Threaded«, d.h., nur ein Javascript-Process läuft im Browser. Der Thread of Execution ist die »Reihenfolge, in der der Code ausgeführt wird« – also der »Arbeitsstrang«, auf dem Anweisungen nacheinander abgearbeitet werden. Im Thread passieren Aktionen wie das Zuweisen von Variablen, der Aufruf von Funktionen, werden Schleifen durchlaufen und Berechnungen ausgeführt.
Darum müssen wir asynchrone Aufrufe nutzen, denn würde das Script Zeile für Zeile ausgeführt, würde der Browser immer wieder hängen, während wir z.B. auf die Antwort eines HTTP-Requests warten.
Holt das Skript Daten mit fetch oder einem asynchronen XMLHttpRequest vom Server, wird die asynchrone Funktion nicht direkt ausgeführt, sondern wartet im Hintergrund auf eine Antwort. Sobald die Anwendung Zeit hat, wird sie ihre Aktion als Antwort auf den Request ausführen.
const prettyPicture = () =>
fetch ("fetch.json")
.then (response => response.json ());
prettyPicture ()
.then (console.log);
document.querySelector("main").classList.add("large");
Sehen wir uns hingegen ein anderes Beispiel an.
const simpleButton = document.getElementById ("simpleButton");
simpleButton.addEventListener ("click", function () {
alert("Button wurde geklickt");
const elem = document.createElement ("p");
elem.innerHTML = "Auf ein Alert wird erbarmungslos gewartet und die Seite ist blockert.";
this.appendChild (elem);
});
Wenn der Benutzer auf den Button klickt, erzeugt Javascript ein modales Fenster – alert. Das Browserfenster ist eingefroren, bis der Benutzer das modale Fenster durch einen Klick schließt. Javascript kann nur eine Anweisung nach der anderen durchführen.
Warten auf ein Event addEventListener
In dynamischen Anwendungen wollen wir vielleicht aber doch auf eine Aktion des Benutzers warten, weil die nächsten Anweisungen je nach Antwort des Benutzers ausfallen.