JavaScript Promises: eine Einführung

Promises vereinfachen verzögerte und asynchrone Berechnungen. Ein Promise stellt einen Vorgang dar, der noch nicht abgeschlossen ist.

Jake Archibald
Jake Archibald

Entwickler, bereiten Sie sich auf einen entscheidenden Moment in der Geschichte der Webentwicklung vor.

[Drumroll begins]

Promise-Objekte sind jetzt in JavaScript verfügbar.

[Feuerwerk explodiert, glitzerndes Papier regnet von oben, die Menge rastet aus]

An diesem Punkt fallen Sie in eine der folgenden Kategorien:

  • Die Leute um dich herum jubeln, aber du bist dir nicht sicher, was der ganze Aufruhr soll. Vielleicht wissen Sie nicht einmal, was ein „Versprechen“ ist. Sie zucken mit den Schultern, aber das Gewicht des glitzernden Papiers lastet auf Ihren Schultern. Falls ja, keine Sorge. Es hat auch bei mir eine Weile gedauert, bis ich verstanden habe, warum ich mich damit beschäftigen sollte. Wahrscheinlich möchten Sie am Anfang beginnen.
  • Du schlägst in die Luft! Das wurde auch Zeit, oder? Sie haben diese Promise-Sachen schon einmal verwendet, aber es stört Sie, dass alle Implementierungen eine leicht unterschiedliche API haben. Welche API wird für die offizielle JavaScript-Version verwendet? Am besten beginnen Sie mit der Terminologie.
  • Du wusstest das schon und belächelst diejenigen, die sich aufregen, als wäre es etwas Neues. Gönnen Sie sich einen Moment, um Ihre Überlegenheit zu genießen, und gehen Sie dann direkt zur API-Referenz.

Browserunterstützung und Polyfill

Browser Support

  • Chrome: 32.
  • Edge: 12.
  • Firefox: 29.
  • Safari: 8.

Source

Wenn Sie Browser, die keine vollständige Implementierung von Promises haben, auf den neuesten Stand bringen oder Promises in anderen Browsern und Node.js hinzufügen möchten, sehen Sie sich das Polyfill an (2 KB komprimiert).

Was hat es damit eigentlich auf sich?

JavaScript ist Single-Threaded. Das bedeutet, dass zwei Skriptteile nicht gleichzeitig ausgeführt werden können, sondern nacheinander. In Browsern teilt sich JavaScript einen Thread mit vielen anderen Dingen, die sich von Browser zu Browser unterscheiden. Normalerweise befindet sich JavaScript jedoch in derselben Warteschlange wie das Rendern, das Aktualisieren von Stilen und die Verarbeitung von Nutzeraktionen (z. B. das Markieren von Text und die Interaktion mit Formularsteuerelementen). Aktivitäten in einem dieser Bereiche verzögern die anderen.

Als Mensch sind Sie multithreaded. Sie können mit mehreren Fingern tippen und gleichzeitig fahren und sich unterhalten. Die einzige blockierende Funktion, mit der wir es zu tun haben, ist das Niesen. Während des Niesens müssen alle aktuellen Aktivitäten unterbrochen werden. Das ist ziemlich nervig, vor allem wenn Sie Auto fahren und sich unterhalten möchten. Sie möchten keinen Code schreiben, der „schnupfig“ ist.

Wahrscheinlich haben Sie Ereignisse und Callbacks verwendet, um dieses Problem zu umgehen. Hier sind einige Beispiele:

var img1 = document.querySelector('.img-1');  img1.addEventListener('load', function() {   // woo yey image loaded });  img1.addEventListener('error', function() {   // argh everything's broken }); 

Das ist überhaupt nicht schnupfig. Wir rufen das Bild ab, fügen ein paar Listener hinzu und dann kann die Ausführung von JavaScript beendet werden, bis einer dieser Listener aufgerufen wird.

Im obigen Beispiel ist es leider möglich, dass die Ereignisse vor dem Start des Listeners stattgefunden haben. Wir müssen das also mit der Eigenschaft „complete“ von Bildern umgehen:

var img1 = document.querySelector('.img-1');  function loaded() {   // woo yey image loaded }  if (img1.complete) {   loaded(); } else {   img1.addEventListener('load', loaded); }  img1.addEventListener('error', function() {   // argh everything's broken }); 

Bilder, bei denen vor dem Abfangen ein Fehler aufgetreten ist, werden nicht erfasst. Leider bietet das DOM keine Möglichkeit, dies zu tun. Außerdem wird nur ein Bild geladen. Noch komplexer wird es, wenn wir wissen möchten, wann eine Reihe von Bildern geladen wurde.

Ereignisse sind nicht immer die beste Lösung

Ereignisse eignen sich gut für Dinge, die mehrmals auf demselben Objekt passieren können, z. B. keyup oder touchstart. Bei diesen Ereignissen ist es nicht wichtig, was passiert ist, bevor Sie den Listener angehängt haben. Bei asynchronen Erfolgs-/Fehlerereignissen sollte es idealerweise so aussehen:

img1.callThisIfLoadedOrWhenLoaded(function() {   // loaded }).orIfFailedCallThis(function() {   // failed });  // and… whenAllTheseHaveLoaded([img1, img2]).callThis(function() {   // all loaded }).orIfSomeFailedCallThis(function() {   // one or more failed }); 

Das ist das, was Promises tun, aber mit einer besseren Namensgebung. Wenn HTML-Bildelemente eine „ready“-Methode hätten, die ein Promise zurückgibt, könnten wir Folgendes tun:

img1.ready() .then(function() {   // loaded }, function() {   // failed });  // and… Promise.all([img1.ready(), img2.ready()]) .then(function() {   // all loaded }, function() {   // one or more failed }); 

Im Grunde sind Promises so etwas wie Event-Listener, aber:

  • Ein Promise kann nur einmal erfolgreich sein oder fehlschlagen. Sie kann nicht zweimal erfolgreich oder fehlgeschlagen sein und auch nicht von Erfolg zu Misserfolg oder umgekehrt wechseln.
  • Wenn ein Promise erfolgreich war oder fehlgeschlagen ist und Sie später einen Erfolgs- oder Fehler-Callback hinzufügen, wird der richtige Callback aufgerufen, auch wenn das Ereignis bereits stattgefunden hat.

Das ist besonders nützlich für asynchrone Erfolgs-/Fehlerereignisse, da Sie weniger daran interessiert sind, wann genau etwas verfügbar wurde, sondern eher darauf reagieren möchten.

Promise-Terminologie

Domenic Denicola hat den ersten Entwurf dieses Artikels Korrektur gelesen und mir für die Terminologie die Note „F“ gegeben. Er gab mir Nachsitzen, zwang mich, States and Fates 100-mal abzuschreiben, und schrieb einen besorgten Brief an meine Eltern. Trotzdem verwechsle ich immer noch viele Begriffe, aber hier sind die Grundlagen:

Ein Versprechen kann Folgendes sein:

  • fulfilled: Die Aktion im Zusammenhang mit dem Versprechen wurde erfolgreich ausgeführt.
  • abgelehnt: Die Aktion im Zusammenhang mit dem Versprechen ist fehlgeschlagen.
  • Ausstehend: Die Anfrage wurde noch nicht erfüllt oder abgelehnt.
  • settled: Die Erfüllung oder Ablehnung ist erfolgt.

In der Spezifikation wird auch der Begriff thenable verwendet, um ein objektähnliches Promise zu beschreiben, das eine then-Methode hat. Dieser Begriff erinnert mich an den ehemaligen englischen Fußballtrainer Terry Venables, daher werde ich ihn so selten wie möglich verwenden.

Promise-Objekte in JavaScript

Promises gibt es schon seit einiger Zeit in Form von Bibliotheken, z. B.:

Die oben genannten und JavaScript-Promises haben ein gemeinsames, standardisiertes Verhalten namens Promises/A+. Wenn Sie jQuery verwenden, gibt es etwas Ähnliches namens Deferreds. Deferreds sind jedoch nicht Promise/A+-kompatibel, was sie etwas anders und weniger nützlich macht. jQuery hat auch einen Promise-Typ, aber dieser ist nur eine Teilmenge von Deferred und hat die gleichen Probleme.

Zwar folgen Promise-Implementierungen einem standardisierten Verhalten, ihre APIs unterscheiden sich jedoch insgesamt. JavaScript-Promise-Objekte ähneln in der API RSVP.js. So erstellen Sie ein Versprechen:

var promise = new Promise(function(resolve, reject) {   // do a thing, possibly async, then…    if (/* everything turned out fine */) {     resolve("Stuff worked!");   }   else {     reject(Error("It broke"));   } }); 

Der Promise-Konstruktor hat ein Argument, einen Callback mit zwei Parametern: „resolve“ und „reject“. Führen Sie etwas innerhalb des Callbacks aus, möglicherweise asynchron, und rufen Sie dann „resolve“ auf, wenn alles funktioniert hat, andernfalls „reject“.

Wie bei throw in einfachem JavaScript ist es üblich, aber nicht erforderlich, mit einem Error-Objekt abzulehnen. Der Vorteil von Fehlerobjekten besteht darin, dass sie einen Stacktrace erfassen, wodurch Debugging-Tools hilfreicher werden.

So nutzen Sie dieses Versprechen:

promise.then(function(result) {   console.log(result); // "Stuff worked!" }, function(err) {   console.log(err); // Error: "It broke" }); 

then() akzeptiert zwei Argumente: einen Callback für den Erfolgsfall und einen für den Fehlerfall. Beide sind optional. Sie können also nur einen Callback für den Erfolgs- oder Fehlerfall hinzufügen.

JavaScript-Promises wurden im DOM als „Futures“ eingeführt, in „Promises“ umbenannt und schließlich in JavaScript verschoben. Dass sie in JavaScript und nicht im DOM enthalten sind, ist von Vorteil, da sie in JS-Kontexten ohne Browser wie Node.js verfügbar sind (ob sie sie in ihren Kern-APIs verwenden, ist eine andere Frage).

Obwohl es sich um eine JavaScript-Funktion handelt, wird sie im DOM häufig verwendet. Tatsächlich werden für alle neuen DOM-APIs mit asynchronen Methoden für Erfolg/Fehler Promises verwendet. Das ist bereits bei Kontingentverwaltung, Schriftart-Ladeereignissen, ServiceWorker, Web MIDI und Streams der Fall.

Kompatibilität mit anderen Bibliotheken

Die JavaScript Promises API behandelt alles mit einer then()-Methode als promise-ähnlich (oder thenable in der Promise-Sprache seufz). Wenn Sie also eine Bibliothek verwenden, die ein Q-Promise zurückgibt, ist das kein Problem. Es funktioniert problemlos mit den neuen JavaScript-Promises.

Wie bereits erwähnt, sind die Deferreds von jQuery jedoch etwas … unpraktisch. Glücklicherweise können Sie sie in Standard-Promises umwandeln. Das sollten Sie so schnell wie möglich tun:

var jsPromise = Promise.resolve($.ajax('/whatever.json')) 

Hier gibt $.ajax von jQuery ein Deferred-Objekt zurück. Da sie eine then()-Methode hat, kann Promise.resolve() sie in ein JavaScript-Promise umwandeln. Manchmal werden jedoch mehrere Argumente an die Rückrufe übergeben, z. B.:

var jqDeferred = $.ajax('/whatever.json');  jqDeferred.then(function(response, statusText, xhrObj) {   // ... }, function(xhrObj, textStatus, err) {   // ... }) 

Bei JS-Promises wird jedoch alles außer dem ersten ignoriert:

jsPromise.then(function(response) {   // ... }, function(xhrObj) {   // ... }) 

Glücklicherweise ist das in der Regel das, was Sie möchten, oder zumindest erhalten Sie dadurch Zugriff auf das, was Sie möchten. Beachten Sie außerdem, dass jQuery nicht der Konvention folgt, Error-Objekte in Ablehnungen zu übergeben.

Komplexer asynchroner Code leicht gemacht

Okay, legen wir los. Nehmen wir an, wir möchten:

  1. Spinner starten, um das Laden anzuzeigen
  2. Rufe JSON für eine Geschichte ab, die uns den Titel und die URLs für jedes Kapitel liefert.
  3. Titel für die Seite hinzufügen
  4. Jedes Kapitel abrufen
  5. Die Geschichte der Seite hinzufügen
  6. Spinner stoppen

… aber auch, wenn etwas schiefgelaufen ist. An diesem Punkt muss der Spinner auch angehalten werden, da er sonst weiterdreht, schwindlig wird und in eine andere Benutzeroberfläche stürzt.

Natürlich würden Sie JavaScript nicht verwenden, um eine Geschichte zu präsentieren, da HTML schneller ist. Dieses Muster ist jedoch bei der Verwendung von APIs sehr häufig: Es werden mehrere Daten abgerufen und dann wird etwas ausgeführt, wenn alles abgeschlossen ist.

Zuerst sehen wir uns an, wie Daten aus dem Netzwerk abgerufen werden:

XMLHttpRequest in Promises umwandeln

Alte APIs werden aktualisiert, um Promises zu verwenden, sofern dies abwärtskompatibel möglich ist. XMLHttpRequest ist ein guter Kandidat, aber in der Zwischenzeit schreiben wir eine einfache Funktion, um eine GET-Anfrage zu stellen:

function get(url) {   // Return a new promise.   return new Promise(function(resolve, reject) {     // Do the usual XHR stuff     var req = new XMLHttpRequest();     req.open('GET', url);      req.onload = function() {       // This is called even on 404 etc       // so check the status       if (req.status == 200) {         // Resolve the promise with the response text         resolve(req.response);       }       else {         // Otherwise reject with the status text         // which will hopefully be a meaningful error         reject(Error(req.statusText));       }     };      // Handle network errors     req.onerror = function() {       reject(Error("Network Error"));     };      // Make the request     req.send();   }); } 

So verwenden Sie es:

get('story.json').then(function(response) {   console.log("Success!", response); }, function(error) {   console.error("Failed!", error); }) 

Jetzt können wir HTTP-Anfragen stellen, ohne XMLHttpRequest manuell eingeben zu müssen. Das ist toll, denn je weniger ich die nervige Kamel-Schreibweise von XMLHttpRequest sehe, desto besser.

Verkettung

then() ist nicht das Ende der Geschichte. Sie können thens verketten, um Werte zu transformieren oder zusätzliche asynchrone Aktionen nacheinander auszuführen.

Werte transformieren

Sie können Werte transformieren, indem Sie einfach den neuen Wert zurückgeben:

var promise = new Promise(function(resolve, reject) {   resolve(1); });  promise.then(function(val) {   console.log(val); // 1   return val + 2; }).then(function(val) {   console.log(val); // 3 }) 

Als praktisches Beispiel kehren wir zu Folgendem zurück:

get('story.json').then(function(response) {   console.log("Success!", response); }) 

Die Antwort ist JSON, wird aber derzeit als Nur-Text empfangen. Wir könnten unsere get-Funktion so ändern, dass sie JSON responseType verwendet, aber wir könnten das Problem auch mit Promises lösen:

get('story.json').then(function(response) {   return JSON.parse(response); }).then(function(response) {   console.log("Yey JSON!", response); }) 

Da JSON.parse() ein einzelnes Argument akzeptiert und einen transformierten Wert zurückgibt, können wir eine Abkürzung verwenden:

get('story.json').then(JSON.parse).then(function(response) {   console.log("Yey JSON!", response); }) 

Tatsächlich können wir ganz einfach eine getJSON()-Funktion erstellen:

function getJSON(url) {   return get(url).then(JSON.parse); } 

getJSON() gibt weiterhin ein Promise zurück, das eine URL abruft und die Antwort als JSON parst.

Asynchrone Aktionen in die Warteschlange stellen

Sie können auch mehrere thens verketten, um asynchrone Aktionen nacheinander auszuführen.

Wenn Sie etwas aus einem then()-Callback zurückgeben, ist das ein bisschen magisch. Wenn Sie einen Wert zurückgeben, wird das nächste then() mit diesem Wert aufgerufen. Wenn Sie jedoch etwas Promise-ähnliches zurückgeben, wartet das nächste then() darauf und wird erst aufgerufen, wenn das Promise abgeschlossen ist (erfolgreich/fehlgeschlagen). Beispiel:

getJSON('story.json').then(function(story) {   return getJSON(story.chapterUrls[0]); }).then(function(chapter1) {   console.log("Got chapter 1!", chapter1); }) 

Hier senden wir eine asynchrone Anfrage an story.json, die uns eine Reihe von URLs liefert, die wir anfordern können. Anschließend fordern wir die erste dieser URLs an. Hier heben sich Promises wirklich von einfachen Callback-Mustern ab.

Sie könnten sogar eine Methode zum Abrufen von Kapiteln erstellen:

var storyPromise;  function getChapter(i) {   storyPromise = storyPromise || getJSON('story.json');    return storyPromise.then(function(story) {     return getJSON(story.chapterUrls[i]);   }) }  // and using it is simple: getChapter(0).then(function(chapter) {   console.log(chapter);   return getChapter(1); }).then(function(chapter) {   console.log(chapter); }) 

Wir laden story.json erst herunter, wenn getChapter aufgerufen wird. Bei den nächsten Aufrufen von getChapter verwenden wir das Story-Promise wieder, sodass story.json nur einmal abgerufen wird. Juhu, Promise-Objekte!

Fehlerbehandlung

Wie wir bereits gesehen haben, akzeptiert then() zwei Argumente: eines für den Erfolg und eines für den Fehler (oder „fulfill“ und „reject“ in der Sprache der Promises):

get('story.json').then(function(response) {   console.log("Success!", response); }, function(error) {   console.log("Failed!", error); }) 

Sie können auch catch() verwenden:

get('story.json').then(function(response) {   console.log("Success!", response); }).catch(function(error) {   console.log("Failed!", error); }) 

catch() ist nichts Besonderes, sondern nur eine Kurzform für then(undefined, func), die besser lesbar ist. Die beiden Codebeispiele oben verhalten sich nicht gleich. Das zweite entspricht Folgendem:

get('story.json').then(function(response) {   console.log("Success!", response); }).then(undefined, function(error) {   console.log("Failed!", error); }) 

Der Unterschied ist zwar klein, aber äußerst nützlich. Bei abgelehnten Promises wird zum nächsten then() mit einem Ablehnungs-Callback (oder catch(), da es gleichwertig ist) gesprungen. Mit then(func1, func2) wird func1 oder func2 aufgerufen, aber nie beide. Mit then(func1).catch(func2) werden jedoch beide aufgerufen, wenn func1 abgelehnt wird, da es sich um separate Schritte in der Kette handelt. Beispiel:

asyncThing1().then(function() {   return asyncThing2(); }).then(function() {   return asyncThing3(); }).catch(function(err) {   return asyncRecovery1(); }).then(function() {   return asyncThing4(); }, function(err) {   return asyncRecovery2(); }).catch(function(err) {   console.log("Don't worry about it"); }).then(function() {   console.log("All done!"); }) 

Der oben beschriebene Ablauf ähnelt dem normalen JavaScript-Try/Catch-Ablauf. Fehler, die innerhalb eines „try“-Blocks auftreten, werden sofort an den catch()-Block weitergeleitet. Hier ist das oben Genannte als Flussdiagramm (weil ich Flussdiagramme liebe):

Folgen Sie den blauen Linien für Promises, die erfüllt werden, oder den roten für Promises, die abgelehnt werden.

JavaScript-Ausnahmen und -Promises

Ablehnungen erfolgen, wenn ein Promise explizit abgelehnt wird, aber auch implizit, wenn im Konstruktor-Callback ein Fehler ausgegeben wird:

var jsonPromise = new Promise(function(resolve, reject) {   // JSON.parse throws an error if you feed it some   // invalid JSON, so this implicitly rejects:   resolve(JSON.parse("This ain't JSON")); });  jsonPromise.then(function(data) {   // This never happens:   console.log("It worked!", data); }).catch(function(err) {   // Instead, this happens:   console.log("It failed!", err); }) 

Daher ist es sinnvoll, alle promise-bezogenen Aufgaben im Callback des Promise-Konstruktors auszuführen, damit Fehler automatisch abgefangen und zu Ablehnungen werden.

Dasselbe gilt für Fehler, die in then()-Callbacks ausgegeben werden.

get('/').then(JSON.parse).then(function() {   // This never happens, '/' is an HTML page, not JSON   // so JSON.parse throws   console.log("It worked!", data); }).catch(function(err) {   // Instead, this happens:   console.log("It failed!", err); }) 

Fehlerbehandlung in der Praxis

Mit unserer Story und den Kapiteln können wir mit „catch“ einen Fehler für den Nutzer anzeigen:

getJSON('story.json').then(function(story) {   return getJSON(story.chapterUrls[0]); }).then(function(chapter1) {   addHtmlToPage(chapter1.html); }).catch(function() {   addTextToPage("Failed to show chapter"); }).then(function() {   document.querySelector('.spinner').style.display = 'none'; }) 

Wenn das Abrufen von story.chapterUrls[0] fehlschlägt (z.B. HTTP 500 oder Nutzer ist offline), werden alle nachfolgenden Erfolgs-Callbacks übersprungen, einschließlich des Callbacks in getJSON(), der versucht, die Antwort als JSON zu parsen, und des Callbacks, der chapter1.html der Seite hinzufügt. Stattdessen wird der Catch-Callback ausgeführt. Wenn eine der vorherigen Aktionen fehlschlägt, wird der Seite die Meldung „Kapitel konnte nicht angezeigt werden“ hinzugefügt.

Wie bei try/catch in JavaScript wird der Fehler abgefangen und der nachfolgende Code wird fortgesetzt. Der Spinner wird also immer ausgeblendet, was wir auch möchten. Das oben Genannte wird zu einer nicht blockierenden asynchronen Version von:

try {   var story = getJSONSync('story.json');   var chapter1 = getJSONSync(story.chapterUrls[0]);   addHtmlToPage(chapter1.html); } catch (e) {   addTextToPage("Failed to show chapter"); } document.querySelector('.spinner').style.display = 'none' 

Möglicherweise möchten Sie catch() nur zu Protokollierungszwecken verwenden, ohne den Fehler zu beheben. Dazu müssen Sie den Fehler nur noch einmal auslösen. Das können wir in unserer getJSON()-Methode tun:

function getJSON(url) {   return get(url).then(JSON.parse).catch(function(err) {     console.log("getJSON failed for", url, err);     throw err;   }); } 

Wir konnten also ein Kapitel abrufen, aber wir möchten alle. Lass uns das ändern.

Parallelität und Sequenzierung: das Beste aus beiden Welten

Asynchron zu denken ist nicht einfach. Wenn Sie Schwierigkeiten haben, mit der Entwicklung zu beginnen, versuchen Sie, den Code so zu schreiben, als wäre er synchron. In diesem Fall gilt:

try {   var story = getJSONSync('story.json');   addHtmlToPage(story.heading);    story.chapterUrls.forEach(function(chapterUrl) {     var chapter = getJSONSync(chapterUrl);     addHtmlToPage(chapter.html);   });    addTextToPage("All done"); } catch (err) {   addTextToPage("Argh, broken: " + err.message); }  document.querySelector('.spinner').style.display = 'none' 

Das funktioniert. Es synchronisiert und sperrt den Browser, während Inhalte heruntergeladen werden. Damit das asynchron funktioniert, verwenden wir then(), um die einzelnen Schritte nacheinander auszuführen.

getJSON('story.json').then(function(story) {   addHtmlToPage(story.heading);    // TODO: for each url in story.chapterUrls, fetch & display }).then(function() {   // And we're all done!   addTextToPage("All done"); }).catch(function(err) {   // Catch any error that happened along the way   addTextToPage("Argh, broken: " + err.message); }).then(function() {   // Always hide the spinner   document.querySelector('.spinner').style.display = 'none'; }) 

Wie können wir die Kapitel-URLs durchlaufen und sie in der richtigen Reihenfolge abrufen? Das funktioniert nicht:

story.chapterUrls.forEach(function(chapterUrl) {   // Fetch chapter   getJSON(chapterUrl).then(function(chapter) {     // and add it to the page     addHtmlToPage(chapter.html);   }); }) 

forEach ist nicht asynchron, daher würden unsere Kapitel in der Reihenfolge angezeigt, in der sie heruntergeladen werden. Das ist im Grunde die Art und Weise, wie Pulp Fiction geschrieben wurde. Das ist nicht Pulp Fiction, also lass uns das beheben.

Sequenz erstellen

Wir möchten unser chapterUrls-Array in eine Folge von Promises umwandeln. Dazu können wir then() verwenden:

// Start off with a promise that always resolves var sequence = Promise.resolve();  // Loop through our chapter urls story.chapterUrls.forEach(function(chapterUrl) {   // Add these actions to the end of the sequence   sequence = sequence.then(function() {     return getJSON(chapterUrl);   }).then(function(chapter) {     addHtmlToPage(chapter.html);   }); }) 

Das ist das erste Mal, dass wir Promise.resolve() sehen. Damit wird ein Promise erstellt, das in den Wert aufgelöst wird, den Sie ihm zuweisen. Wenn Sie eine Instanz von Promise übergeben, wird sie einfach zurückgegeben. Hinweis:Dies ist eine Änderung der Spezifikation, die in einigen Implementierungen noch nicht berücksichtigt wird. Wenn Sie ein Promise-ähnliches Objekt (mit einer then()-Methode) übergeben, wird ein echtes Promise-Objekt erstellt, das auf dieselbe Weise erfüllt oder abgelehnt wird. Wenn Sie einen anderen Wert übergeben, z.B. Promise.resolve('Hello') wird ein Promise erstellt, das mit diesem Wert erfüllt wird. Wenn Sie die Funktion ohne Wert aufrufen, wie oben, wird „undefined“ zurückgegeben.

Es gibt auch Promise.reject(val), das ein Promise erstellt, das mit dem von Ihnen angegebenen Wert (oder „undefined“) abgelehnt wird.

Mit array.reduce können wir den oben stehenden Code optimieren:

// Loop through our chapter urls story.chapterUrls.reduce(function(sequence, chapterUrl) {   // Add these actions to the end of the sequence   return sequence.then(function() {     return getJSON(chapterUrl);   }).then(function(chapter) {     addHtmlToPage(chapter.html);   }); }, Promise.resolve()) 

Das Ergebnis ist dasselbe wie im vorherigen Beispiel, aber es ist keine separate Variable „sequence“ erforderlich. Die Reduce-Callback-Funktion wird für jedes Element im Array aufgerufen. „sequence“ ist beim ersten Mal Promise.resolve(), aber für die restlichen Aufrufe ist „sequence“ das, was wir beim vorherigen Aufruf zurückgegeben haben. array.reduce ist sehr nützlich, um ein Array auf einen einzelnen Wert zu reduzieren, der in diesem Fall ein Promise ist.

Zusammenfassung:

getJSON('story.json').then(function(story) {   addHtmlToPage(story.heading);    return story.chapterUrls.reduce(function(sequence, chapterUrl) {     // Once the last chapter's promise is done…     return sequence.then(function() {       // …fetch the next chapter       return getJSON(chapterUrl);     }).then(function(chapter) {       // and add it to the page       addHtmlToPage(chapter.html);     });   }, Promise.resolve()); }).then(function() {   // And we're all done!   addTextToPage("All done"); }).catch(function(err) {   // Catch any error that happened along the way   addTextToPage("Argh, broken: " + err.message); }).then(function() {   // Always hide the spinner   document.querySelector('.spinner').style.display = 'none'; }) 

Und das war’s auch schon – wir haben eine vollständig asynchrone Version der synchronen Version. Aber wir können es besser machen. Derzeit wird unsere Seite so heruntergeladen:

Browser sind ziemlich gut darin, mehrere Dinge gleichzeitig herunterzuladen. Wenn wir Kapitel nacheinander herunterladen, verlieren wir also Leistung. Wir möchten sie alle gleichzeitig herunterladen und dann verarbeiten, wenn sie alle angekommen sind. Glücklicherweise gibt es dafür eine API:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {   //... }) 

Promise.all nimmt ein Array von Promises entgegen und erstellt ein Promise, das erfüllt wird, wenn alle Promises erfolgreich abgeschlossen wurden. Sie erhalten ein Array mit Ergebnissen (was auch immer die Promises erfüllt haben) in derselben Reihenfolge wie die Promises, die Sie übergeben haben.

getJSON('story.json').then(function(story) {   addHtmlToPage(story.heading);    // Take an array of promises and wait on them all   return Promise.all(     // Map our array of chapter urls to     // an array of chapter json promises     story.chapterUrls.map(getJSON)   ); }).then(function(chapters) {   // Now we have the chapters jsons in order! Loop through…   chapters.forEach(function(chapter) {     // …and add to the page     addHtmlToPage(chapter.html);   });   addTextToPage("All done"); }).catch(function(err) {   // catch any error that happened so far   addTextToPage("Argh, broken: " + err.message); }).then(function() {   document.querySelector('.spinner').style.display = 'none'; }) 

Je nach Verbindung kann das Laden auf diese Weise Sekunden schneller sein als das Laden einzelner Bilder. Außerdem ist weniger Code erforderlich als bei unserem ersten Versuch. Die Kapitel können in beliebiger Reihenfolge heruntergeladen werden, werden aber auf dem Bildschirm in der richtigen Reihenfolge angezeigt.

Wir können die wahrgenommene Leistung jedoch weiterhin verbessern. Wenn das erste Kapitel eintrifft, sollten wir es der Seite hinzufügen. So kann der Nutzer mit dem Lesen beginnen, bevor die restlichen Kapitel eingegangen sind. Wenn Kapitel 3 erscheint, fügen wir es nicht auf der Seite hinzu, da der Nutzer möglicherweise nicht bemerkt, dass Kapitel 2 fehlt. Wenn Kapitel 2 fertig ist, können wir Kapitel 2 und 3 hinzufügen usw.

Dazu rufen wir gleichzeitig JSON für alle Kapitel ab und erstellen dann eine Sequenz, um sie dem Dokument hinzuzufügen:

getJSON('story.json') .then(function(story) {   addHtmlToPage(story.heading);    // Map our array of chapter urls to   // an array of chapter json promises.   // This makes sure they all download in parallel.   return story.chapterUrls.map(getJSON)     .reduce(function(sequence, chapterPromise) {       // Use reduce to chain the promises together,       // adding content to the page for each chapter       return sequence       .then(function() {         // Wait for everything in the sequence so far,         // then wait for this chapter to arrive.         return chapterPromise;       }).then(function(chapter) {         addHtmlToPage(chapter.html);       });     }, Promise.resolve()); }).then(function() {   addTextToPage("All done"); }).catch(function(err) {   // catch any error that happened along the way   addTextToPage("Argh, broken: " + err.message); }).then(function() {   document.querySelector('.spinner').style.display = 'none'; }) 

So haben Sie das Beste aus beiden Welten. Es dauert genauso lange, bis alle Inhalte ausgeliefert werden, aber der Nutzer erhält den ersten Teil der Inhalte früher.

In diesem einfachen Beispiel kommen alle Kapitel ungefähr zur selben Zeit an. Der Vorteil der Anzeige eines Kapitels nach dem anderen wird jedoch bei mehr und größeren Kapiteln deutlicher.

Wenn Sie das oben Genannte mit Rückrufen oder Ereignissen im Node.js-Stil ausführen, ist der Code etwa doppelt so lang und vor allem nicht so einfach nachzuvollziehen. Das ist aber noch nicht alles, was Promises zu bieten haben. In Kombination mit anderen ES6-Funktionen werden sie noch einfacher.

Zusatzrunde: Erweiterte Funktionen

Seit ich diesen Artikel ursprünglich geschrieben habe, hat sich die Möglichkeit, Promises zu verwenden, erheblich erweitert. Seit Chrome 55 können mit asynchronen Funktionen Promise-basierte Code so geschrieben werden, als wäre er synchron, ohne den Hauptthread zu blockieren. Weitere Informationen dazu finden Sie in diesem Artikel. Sowohl Promises als auch asynchrone Funktionen werden von den wichtigsten Browsern unterstützt. Weitere Informationen finden Sie in den MDN-Referenzen zu Promise und async function.

Vielen Dank an Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans und Yutaka Hirano, die diesen Artikel Korrektur gelesen und Korrekturen/Empfehlungen vorgenommen haben.

Vielen Dank auch an Mathias Bynens für die Aktualisierung verschiedener Teile des Artikels.