Callbacks bei zeitlich unbestimmbaren Abläufen
Javascript Promise ist mit ECMAScript 6 eingezogen, macht den Umgang mit asynchronem Verhalten sicherer und durch die then-Funktion lesbarer als Callback-Funktionen. Asynchron bedeutet, dass zwischen einer Anfrage und der Antwort eine Verzögerung oder Wartezeit von unbestimmter Dauer liegt.
Wenn wir auf einen zeitlich nicht absehbaren Ablauf angewiesen sind – z.B. beim Laden eines Bildes oder einer json-Datei – werden Callbacks eingesetzt, die beim Eintreten eines Ereignisses aufgerufen werden.
Da die Callback-Funktion i.d.R. nur einmal aufgerufen wird, sehen wir Callbacks oft als anonyme Funktion. Promises ergänzen oder ersetzen Callbacks und sind syntaktisch prägnanter sind als Rückrufe. Sie geben dem Code eine besser lesbare Struktur und zusätzliche Garantien, die den asynchronen Ablauf verläßlicher machen.
Asynchrone Events und das Warten
Wenn Daten geladen werden, dauert das Warten immer eine unbestimmte Zeit. Wenn nur ein Ladevorgang beobachtet werden muss, ist alles noch einfach. Aber was ist, wenn nach dem Laden der JSON-Datei auf eine weitere Aktion gewartet werden muss, z.B. weil die URL der zweiten JSON-Datei in der ersten URL steht?
const url = "file.json";
const xhr = new XMLHttpRequest(url);
xhr.addEventListener ("readystatechange", (url) => {
if (xhr.readyState === 4) {
console.log (xhr.responseText);
}
})
xhr.open ("GET", url);
xhr.send();
function konzerte (json, myCallback) { const xhr = new XMLHttpRequest (); xhr.open ("GET", json); xhr.onreadystatechange = function () { if ((this.readyState === 4) & (this.status === 200)) { myCallback (JSON.parse (this.responseText)); } } xhr.send(); } function plan (data) { data.forEach (event => { const li = document.createElement ("li"); li.innerText = `${event.datum} ${event.konzert}`; document.querySelector ("ul").append (li) }) } konzerte ("konzerte-10.json", (data) => { plan (data); konzerte ("konzerte-11.json", (data) => { plan (data); konzerte ("konzerte-12.json", (data) => { plan (data); }) }) });
Javascript Promise
Mit verschachtelten asynchronen Events kommen wir in Teufels Küche, die auch als Callback Hell bezeichnet wird. Auch jQuery und Javascript fetch würden dieses abgrundtiefe Loch verschachtelter asynchroner Aufrufe nicht wirklich vereinfachen.
Promises sind Objekte, die das Ergebnis einer asynchronen Aktion abfangen und es zurück geben, wenn die versprochenen Daten erfolgreich geladen wurden oder ein Fehler aufgetreten ist. Sie sind sozusagen Platzhalter für eine Zusage, die vielleicht erfüllt wird, vielleicht aber auch nicht.
let p = new Promise ( function (resolve, reject) { if (alle Aufgaben erfüllt) { resolve (); } else { reject (err); } });
Ein Promise ist eine Schutzmaßnahme. Es wird niemals vor dem Ende des Wartens auf die Ausführung einer Aktion aufgerufen und wird nur einmal ausgeführt. Gleich ob ein Promise erfüllt werden kann oder nicht: Es ruft auf jeden Fall die korrekte Methode resolve () bzw. reject () auf. Darüber hinaus können then und catch miteinander verketted (chaining) werden: Dann ruft das Ergebnis einer Operation am Ende die nächste Operation auf.
Promise und then ()
Ein einfacher XMLHttpRequest mit nur einem asynchronen Request ist übersichtlich, aber sobald ein weiterer Request auf dem ersten Request beruht, wird der Code verschachtelt und unübersichtlich.
Promises stellen eine Methode then () zur Verfügung, die ausgeführt wird, nachdem das Versprechen eingelöst wurde. then () enthält zwei optionale Argumente: ein Callback für den Erfolg, ein Callback für den Fehlerfall.
p.then ( function (result) { // Promise erfüllt }, function (err) { // Promise zurückgewiesen })
Wenn das Promise erfüllt ist, wird die erste Funktion mit dem übergebenen result-Objekt ausgeführt. Anderenfalls wird die Rückweisung (reject) in der zweiten Funktion mit dem übergebenen error-Objekt ausgeführt.
So kann z.B. eine komplizierte Aktionsfolge aussehen, in der drei JSON-Dateien geladen werden, wobei das Auslesen der zweiten JSON-Datei nur Sinn macht, wenn das erste JSON korrekt angeliefert wurde.
Alternativ kann das Callback aufgesplittet werden und catch übernimmt das Zurückweisen des Promise. Die Methoden können verkettet und sequentiell ausgeführt werden. Wenn etwas von then zurückgegeben wird, wird es dem nächsten then übergeben.
function konzert (json) { return new Promise ((resolve, reject) => { const xhr = new XMLHttpRequest (); xhr.open ("GET", json); xhr.onreadystatechange = function () { if (this.readyState === 4) { if (this.status === 200) { resolve (JSON.parse (this.responseText)) } else { reject ("konnte nicht geladen werden"); } } } xhr.send(); }); }
then-Aufrufe können sequentiell aneinander gehangen werden, um zusätzliche asynchrone Aktionen nacheinander auszuführen (chaining).
Wird ein Promise zurückgegeben, wartet das nächste then auf das erste Promise, bevor die Ausführung beginnt.
konzert ("promise/konzerte-10.json") .then ((events10) => { plan (events10) return konzert ("promise/konzerte-11.json"); }) .then ((events11) => { plan (events11) return konzert ("promise/konzerte-12.json"); }) .then ((events12) => { plan (events12) });
Da haben wir ein einfaches Muster: then gefolgt von einem return, then gefolgt von einem return, alles aufgereiht wie auf einer Perlenschnur und ohne Verschachtelung.
In diesem Beispiel ist übrigens kein catch-Block mit im Spiel – auch das geht ohne Weiteres, denn das catch ist optional. Es sollte nur dafür gesorgt werden, dass die Anwendung »sanft« verweigert.
Zuguterletzt noch die Funktion plan (), die alle Konzerte in zeitlicher Folge ausgibt.
function plan (data) { data.forEach (event => { const li = document.createElement ("li"); li.innerText = `${event.datum} ${event.konzert}`; document.querySelector ("ul.plan1").append (li) }) }
Promise vs Event Listener
Promises ähneln Event Listenern mit ihrem asynchronen Callback in gewisser Hinsicht. Wir legen fest, was bei der Erfüllung oder bei einem Fehler passieren soll und der Callback wartet auf den Ausgang des Promise.
Der Unterschied: Während Events mehrmals feuern können, findet ein Promise nur einmal statt.
Promise.all () – mehrere Dateien laden
In solchen Situationen wie dem vorangegangenen Beispiel gibt es eine elegante Lösung mit Promise.all (). Das Argument von all() ist ein Array. Die Funktion konzerte () bleibt wie gehabt, aber die Arrays der JSON-Dateien werden einer Variablen zugewiesen.
const events10 = konzerte ("konzerte-10.json"); const events11 = konzerte ("konzerte-11.json"); const events12 = konzerte ("konzerte-12.json");
Promise.all ([events10, events11, events12] ) .then ((data) => { data.forEach ((elem) => plan(elem)); }) .catch ((error) => console.log (error));
Browser-Unterstützung für promise
Sowohl promise als auch fetch werden von den immergrünen Browsern seit geraumer Zeit unterstützt und wie immer steht IE11 außen vor. Promise und Fetch sind keine Konstrukte der Programmiersprache, also muss nicht gleich ein Transpiler her, sondern Polyfills reichen schon.
Der XMLHttpRequest
Der XMLHttpRequest ist aus der Zeit gefallen. fetch gibt ein Promise zurück und vereinfacht das Laden von Daten und die API-Programmierung. Dank fetch und async await ist es selten geworden, dass wir eigene Promise-Objekte einsetzen.