Asynchrone Events, Callback und Promise
Javascript kann Aufgaben »nebenbei« abhandeln – während der Browser eine Seite lädt oder Javascript bereits eine Funktion ausführt, kann das Script auf das Eintreten eines Ereignisses (z.B. auf den Klick auf einen Button) reagieren.
Synchrone Aktionen vs. asynchrone Verarbeitung
JavaScript ist eine synchrone Programmiersprache: Der Script-Code wird Zeile für Zeile, zu einer Zeit immer nur eine Anweisung ausgeführt. Synchrone Aktionen bilden eine lineare Kette und sind einfach nachvollziehbar:
- Mathematische Operationen wie
let res = 17 + 4; console.log ("res", res) - Zuweisungen an Variablen
const a = 17; const b = 4; const sum = a + b;
- String-Operationen
const strg = "Hallo"; let start = strg + " Welt!"; console.log (start)
- Funktionsaufrufe
let betrag = summe(a, b)
JavaScript führt aber auch 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.
- der XMLHTTPRequest,
- fetch,
- setTimeout
- requestAnimationFrame
- addEventListener
Für die Behandlung asynchroner Aufgaben stellt Javascript mehrere Optionen zur Verfügung:
- Callback-Funktionen
- Promise
- Async / Await
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 du einer anderen Funktion übergibst, 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 + "'>";
})();