Viele JavaScript-Frameworks wie jQuery und AngularJS kennen sogenannte Promises. Sie ermöglichen es, das Ergebnis asynchroner Aufrufe festzustellen und auf jenes Ergebnis zuzugreifen. Dabei lassen sich mehrere asynchrone Aufrufe „in Reihe schalten“. So wird erreicht, dass ein Aufruf erst gestartet wird, wenn ein anderer Aufruf bereits erfolgreich abgeschlossen wurde. Mittlerweile gibt es Promises auch als natives JavaScript-Objekt. So kann es ohne zusätzliche Frameworks und Bibliotheken eingesetzt werden.
Wozu braucht man Promises?
Gerade in komplexen Webanwendungen kommt man ganz oft nicht umhin, per Events und Callbacks festzustellen, ob eine Operation erfolgreich abgeschlossen werden konnte – beispielsweise, wenn man Inhalte per „XMLHttpRequest()“ lädt und diese anschließend weiterverarbeiten will. Mit dem „load“-Event lässt sich hier über eine Callback-Funktion feststellen, wie der aktuelle Status des „XMLHttpRequest()“-Aufrufs ist.
function bild_laden(url) {
var datei = new XMLHttpRequest();
datei.open("GET", url);
datei.addEventListener("load", function() {
if (this.status == 200) {
console.log(url + " geladen.");
} else {
console.log(url + "nicht geladen.");
}
}, false);
datei.send();
}
bild_laden("bild.jpg");
Das simple Beispiel zeigt, wie Callbacks funktionieren. Das „load“-Event ruft eine Funktion auf, sobald der XMLHttpRequest()“-Aufruf erfolgreich war oder gescheitert ist. Der Status 200 im Beispiel wird ausgegeben, wenn das Laden erfolgreich war. Schwierig wird es allerdings, wenn beispielsweise eine zweite Datei geladen werden soll – und zwar, nachdem die erste geladen wurde.
bild_laden("bild1.jpg");
bild_laden("bild2.jpg");
Man kann natürlich die Funktion zweimal aufrufen und die beiden zu ladenden Dateinamen übergeben. Allerdings weiß man nie, welcher der beiden Requests als erster fertig ist. Bei den beiden Bildern im Beispiel dürfte es egal sein, welche Datei zuerst geladen wurde. Aber es gibt immer wieder Abläufe, bei denen man sicherstellen muss, dass bestimmte Inhalte bereits zur Verfügung stehen, bevor andere Inhalte geladen oder andere Operationen ausgeführt werden.
Jetzt kann man sich unterschiedlich behelfen, um festzustellen, ob eine Datei bereits geladen wurde. Man kann beispielsweise Events und die dazugehörigen Callback-Funktionen verschachteln.
function bild_laden(url1, url2) {
var datei1 = new XMLHttpRequest();
datei1.open("GET", url1);
datei1.addEventListener("load", function() {
if (this.status == 200) {
console.log(url + " geladen.");
var datei2 = new XMLHttpRequest();
datei2.open("GET", url2);
datei2.addEventListener("load", function() {
if (this.status == 200) {
console.log(url + " geladen.");
} else {
console.log(url + " nicht geladen.");
}
}, false);
datei2.send();
} else {
console.log(url + " nicht geladen.");
}
}, false);
datei1.send();
}
bild_laden("bild.jpg", "bild.jpg");
Das zweite Beispiel zeigt, dass bereits die Verschachtelung zweier Callback-Funktionen die ganze Sache recht komplex macht. Zwar erreicht man, dass die zweite Datei erst geladen wird, nachdem die erste erfolgreich geladen wurde. Kommen allerdings noch weitere Aufrufe dazu, die vom Erfolg anderer Aufrufe abhängig gemacht werden sollen, wird es für Programmierer oft sehr unübersichtlich. Nicht ohne Grund nennt man diese Problematik auch „Pyramid of Doom“.
Mit Promises gibt es eine wesentlich elegantere Möglichkeit, den Erfolg verschiedener Operationen festzustellen. Statt verschachtelter Callback-Funktionen wird der Erfolg beziehungsweise Misserfolg über ein sogenanntes Promise-Objekt festgestellt. Dabei haben Promises noch weitere Vorteile gegenüber einfacher Callbacks, die an späterer Stelle noch vorgestellt werden.
Neuen Promise anlegen
Um mit Promises zu arbeiten, muss zunächst ein neues Promise-Objekt angelegt werden. Diesem wird eine Funktion zugewiesen, die zwei Werte zurückgibt:
Der erste Wert – „Resolve“ genannt – wird zurückgegeben, wenn der Promise erfolgreich war, der zweite Wert – „Reject“ genannt –, wenn der Promise nicht erfolgreich war.
function bild_laden(url) {
return new Promise(function(erfolg, misserfolg) {
var datei = new XMLHttpRequest();
datei.open("GET", url);
datei.addEventListener("load", function() {
if (this.status == 200) {
erfolg(url + " geladen.");
} else {
misserfolg(url + " nicht geladen.");
}
}, false);
datei.send();
});
}
Im Beispiel wird der Funktion „bild_laden()“ per „return“ ein neues Promise-Objekt hinzugefügt. Das „return“ sorgt dafür, dass die Funktion das Ergebnis des Promises zurückgibt – also die Werte für „Resolve“ beziehungsweise „Reject“. Das Promise-Objekt enthält wiederum eine Funktion, die per „XMLHttpRequest()“ eine Datei lädt. Im Grunde wird dasselbe gemacht wie im ersten Beispiel. Auch hier wird per Callback-Funktion über das „load“-Event geprüft, ob die Datei geladen wurde oder nicht. Im Erfolgsfall wird dem „Resolve“ mit der Bezeichnung „erfolg“ ein Wert zugewiesen. Dieser Wert kann frei vergeben werden. Dem „Resolve“ kann auch der Inhalt der geladenen Datei („datei.response“) übergeben werden. So kann man später darauf zugreifen.
Der Wert für „Reject“ – im Beispiel „misserfolg“ – kann ebenfalls frei vergeben werden. Dieser wird meist verwendet, um eine Fehlermeldung auszugeben.
Der große Vorteil der Promises ist, dass das erfolgreiche Laden der Datei per „XMLHttpRequest()“ jenseits der Funktion „bild_laden()“ geprüft werden kann. Man muss Callback-Funktionen also nicht verschachteln. Dafür stehen die beiden Methoden „then()“ und „catch()“ zur Verfügung.
bild_laden("bild.jpg").then(
function(erfolg) {
console.log(erfolg);
}
).catch(
function(misserfolg) {
console.log(misserfolg);
}
);
Im Beispiel wird das Promise-Objekt beziehungsweise die Funktion „bild_laden()“ aufgerufen, um eine Datei zu laden. Die Methode „then()“ des Promise-Objektes wird dann ausgeführt, wenn der Promise erfolgreich war – die Datei also geladen wurde. Andernfalls wird die „catch()“-Methode ausgeführt. Während der „then()“-Methode der Inhalt von „Resolve“ – im Beispiel „erfolg“ – übergeben wird, erhält die „catch()“-Methode den Inhalt von „Reject“ – im Beispiel „misserfolg“. Über eine Funktion können die Inhalte der beiden übergebenen Variablen ausgegeben oder anderweitig verarbeitet werden. Im Beispiel werden die Werte einfach in die Konsole geschrieben.
Um mehrere Promise-Aufrufe „in Reihe zu schalten“, genügt es, mehrere „then()“-Methoden miteinander zu verketten. So wird erreicht, dass die erste „then()“-Methode erst dann aufgerufen wird, wenn der erste Promise erfolgreich war.
bild_laden("bild1.jpg").then(
bild_laden("bild2.jpg")
).then(
bild_laden("bild3.jpg")
).catch(
function(misserfolg) {
console.log(misserfolg);
}
);
Das Beispiel zeigt, wie einfach Aufrufe verkettet werden können. Nachdem die Datei „bild1.jpg“ erfolgreich geladen wurde, wird per „then()“ die Datei „bild2.jpg“ geladen. Ist auch diese Datei geladen, wird eine dritte Datei geladen. Die „catch()“-Methode im Beispiel wird aufgerufen, wenn die erste Datei nicht geladen werden konnte – der erste Promise also erfolglos war.
Natürlich kann für jeden einzelnen Aufruf eine „catch()“-Methode ergänzt werden. So lässt sich jeder nicht erfolgreiche Aufruf der Funktion abfangen.
bild_laden("bild1.jpg").then(
bild_laden("bild2.jpg")
).then(
bild_laden("bild3.jpg").catch(
function(misserfolg) {
console.log(misserfolg);
}
)
).then(
bild_laden("bild2.jpg").catch(
function(misserfolg) {
console.log(misserfolg);
}
)
).catch(
function(misserfolg) {
console.log(misserfolg);
}
);
Dieses Beispiel zeigt, wie für jeden Aufruf der Funktion „bild_laden()“ eine entsprechende „catch()“-Methode ergänzt wurde. So entgeht einem kein erfolgloser Promise.
Ein Promise kann einen von drei Stati besitzen. Während geprüft wird, ob ein Promise erfüllt ist oder nicht, besitzt er den Status „pending“. Ist er erfüllt, hat er den Status „fulfilled“. Scheitert ein Promise, ist sein Status „rejected“. Der Status selbst lässt sich nicht abfragen, ist für das Verständnis von Promises aber nicht unwichtig.
Promises als Array übergeben und prüfen
Statt einzelne Funktionen per „then()“ aneinanderzuketten, gibt es auch eine einfachere Möglichkeit. Die Methode „all()“ erlaubt es, Funktionen beziehungsweise Promise-Objekte als Array zu übergeben und dieses Array als Ganzes zu prüfen.
Promise.all([bild_laden("bild1.jpg"), bild_laden("bild2.jpg")]).then(
function (erfolg) {
console.log(erfolg);
}
);
Im Beispiel werden die einzelnen Aufrufe von „bild_laden()“ als Array der „all()“-Methode übergeben. Der Vorteil ist die wesentliche kürzere Auszeichnung. Allerdings hat man hierbei keine Möglichkeit, auf einzelne Misserfolge zu reagieren. Die „then()“-Methode wird nur dann ausgeführt, wenn alle Promises im Array erfüllt werden – also alle Dateien geladen sind. Sobald ein Promise im Array erfolglos war, gibt „all()“ für den gesamten Array den Status „rejected“ aus. Allerdings werden beim Status „fulfilled“ alle Rückgabewerte des Arrays („erfolg“) ausgegeben. Diese werden ebenfalls als Array bereitgestellt und im Beispiel in die Konsole geschrieben.
Schnellste Funktion wiedergeben
Neben der Methode „all()“ gibt es noch die Methode „race()“, bei der verschiedene Promises ebenfalls als Array übergeben werden. Statt alle Promises auszuführen und wiederzugeben, trifft das bei „race()“ nur auf den ersten erfolgreichen oder gescheiterten Promise zu.
Promise.race([bild_laden("bild1.jpg"), bild_laden("bild2.jpg")]).then(
function (erfolg) {
console.log(erfolg);
}
);
Im Beispiel wird also nur für den ersten Promise, der erfolgreich oder nicht erfolgreich war, die Methode „then()“ aufgerufen. Relevant ist hierbei nur, welcher der Promises als erster einen Erfolg oder Misserfolg meldet.
Verkürzte Schreibweise für „catch()“
Auf die Auszeichnung der „catch()“-Methode kann auch verzichtet werden. Es gibt eine verkürzte Möglichkeit. Der „then()“-Methode können zwei Funktionen übergeben werden. Die erste der Funktionen wird dann beim Erfolg eines Promises ausgeführt, die zweite beim Misserfolg. Die zweite Funktion entspricht also der innerhalb der „catch()“-Methode.
bild_laden("bild.jpg").then(
function(erfolg) {
console.log(erfolg);
}, function(misserfolg) {
console.log(misserfolg);
}
);
Die beiden Funktionen werden einfach per Komma voneinander getrennt. Will man nur eine Funktion für den Misserfolg definieren, genügt es, anstelle der ersten Funktion ein „undefined“ zu notieren.
Promises testen mit „resolve()“ und „reject()“
Mit den beiden Methoden „resolve()“ und „reject()“ kann man den Erfolg beziehungsweise Misserfolg eines Promises testen. Die „resolve()“-Methode gibt immer ein Promise-Objekt wieder, welches erfolgreich war – unabhängig davon, ob es tatsächlich von Erfolg gekrönt war. Die „reject()“-Methode hingegen stuft ein Promise-Objekt immer als nicht erfolgreich ein. In beiden Fällen werden die jeweiligen Werte von „Resolve“ beziehungsweise „Reject“ an die Methoden übergeben.
Promise.reject(bild_laden("bild1.jpg")).catch(
function(misserfolg) {
console.log(misserfolg);
}
);
Im Beispiel wird per „reject()“ dafür gesorgt, dass die Funktion beziehungsweise das Promise-Objekt „bild_laden()“ als nicht erfolgreich wiedergegeben wird. Da die „then()“-Methode nur im Erfolgsfall aufgerufen wird, muss sie nicht aufgeführt werden. Denn dieser wird bei der „reject()“-Methode nie eintreten. Daher genügt es, die „catch()“-Methode aufzurufen.
Browsersupport und Polyfills
Promises laufen unter Chrome ab Version 32, Firefox ab Version 29 und Opera ab Version 19. Beim Internet Explorer sind Promises in der Entwicklung und werden möglicherweise in der nächsten Verison unterstützt. Es gibt auch ein Polyfill, der Promises für ältere Browser nachbildet.
Neben dem nativen Promise-Objekt und den Implementierungen in verschiedenen Frameworks wie jQuery und AngularJS gibt es auch Standalone-Frameworks für Promises. Dazu zählen unter anderem „Q“ sowie „Promise.js“. Diese Frameworks sollte man nicht vernachlässigen, nur weil es jetzt eine native JavaScript-Möglichkeit für Promises gibt. Denn die Promise-Objekte der Frameworks haben teils umfangreichere Features als das, was JavaScript nativ derzeit anbietet. Wer sich also weiter mit Promises auseinandersetzen will, sollte „Q“, „Promise.js“ und den anderen Frameworks durchaus mal etwas Aufmerksamkeit schenken.
Um festzustellen, ob der Browser native Promises unterstützt, genügt übrigens eine einfache „if“-Abfrage.
if (window.Promise) {
…
}
Links zum Beitrag
- ES6-Promise (Polyfill)
- Promise.js
- Q
(dpe)
Wie hilfreich war dieser Beitrag?
Klicke auf die Sterne um zu bewerten!
Durchschnittliche Bewertung 0 / 5. Anzahl Bewertungen: 0
Eine Antwort zu „JavaScript: mit nativen Promises auf Callback-Verschachtelungen verzichten“
— was ist Deine Meinung?
Aktuelle Browser Unterstützung?