JavaScript Promises: eine Einführung

Promise-Objekte vereinfachen verzögerte und asynchrone Berechnungen. Ein Promise stellt einen noch nicht abgeschlossenen Vorgang dar.

Jake Archibald
Jake Archibald

Bereiten Sie sich auf einen entscheidenden Moment in der Geschichte des Webentwicklung.

[Trommelwirbel beginnt]

Promise-Objekte sind in JavaScript eingegangen.

[Feuerwerk explodieren, glitzerndes Papier regnet von oben, die Menge ist wild]

An dieser Stelle fallen Sie in eine der folgenden Kategorien:

  • Die Leute jubeln um Sie herum, aber Sie sind sich nicht sicher, was das Ganze ist über. Vielleicht sind Sie sich nicht einmal sicher, ist. Sie würden die Achseln zucken, aber Das Gewicht von glitzerndem Papier liegt auf Ihren Schultern. Wenn ja, sollten Sie Es dauerte eine ganze Weile, bis ich herausgefunden hatte, Vielleicht möchten Sie von vorn beginnen.
  • Du schlagst die Luft! Zeit, richtig? Du hast diese Promise schon einmal verwendet Allerdings stört es Sie, dass alle Implementierungen eine etwas andere API haben. Wie lautet die API für die offizielle JavaScript-Version? Beginnen Sie am besten damit, durch die Terminologie.
  • Das wussten Sie schon und Sie verspotten diejenigen, die aufspringen und wie Nachrichten für sie. Nehmen Sie sich einen Moment Zeit, Rufen Sie dann direkt die API-Referenz auf.

Browserunterstützung und Polyfill

Unterstützte Browser

  • Chrome: 32. <ph type="x-smartling-placeholder">
  • Rand: 12. <ph type="x-smartling-placeholder">
  • Firefox: 29. <ph type="x-smartling-placeholder">
  • Safari: 8. <ph type="x-smartling-placeholder">

Quelle

Um Browser ohne vollständige Versprechen an die Spezifikationen anzupassen oder anderen Browsern und Node.js Versprechen hinzufügen, Polyfill (2 K gzip-Datei)

Was soll das Ganze?

JavaScript ist ein Single-Thread-Element, d. h., zwei Skripte können nicht gleichzeitig ausgeführt werden. zur gleichen Zeit wechseln. müssen sie nacheinander ausgeführt werden. In Browsern zeigt JavaScript einen Thread mit einer Vielzahl anderer Inhalte teilt, die sich von Browser zu Browser unterscheiden. Browser. Normalerweise befindet sich JavaScript in derselben Warteschlange wie das Painting, sowie die Verarbeitung von Nutzeraktionen (z. B. Hervorheben von Text und Interaktionen mit Formularsteuerelementen). Aktivitäten in einem dieser Dinge verzögern die anderen.

Mensch ist ein Multithread. Sie können Text mit mehreren Fingern eingeben, Sie können gleichzeitig eine Unterhaltung führen und führen. Die einzige Blockierung mit der wir uns befassen müssen, ist das Niesen, bei dem alle aktuellen Aktivitäten während des Niesens ausgesetzt werden. Das ist ziemlich ärgerlich, vor allem, wenn Sie Auto fahren und versuchen, ein Gespräch zu führen. Das solltest du nicht tun Code schreiben wollen, der einfach nicht passt.

Um dies zu umgehen, haben Sie wahrscheinlich Ereignisse und Callbacks verwendet. Hier sind Ereignisse:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

Das ist gar nicht lustig. Wir rufen das Bild ab, fügen einige Zuhörer hinzu JavaScript kann die Ausführung stoppen, bis einer dieser Listener aufgerufen wird.

Leider ist es im obigen Beispiel möglich, dass die Ereignisse bevor wir sie hören. Deshalb müssen wir die „vollständigen“ Eigenschaft von Bildern:

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 mit Fehlern werden nicht erkannt, bevor wir Leider bietet das DOM dafür keine Möglichkeit. Dies ist außerdem wird ein Bild geladen. Es wird noch komplexer, wenn wir wissen möchten, von Bildern wurden geladen.

Veranstaltungen sind nicht immer optimal

Ereignisse eignen sich hervorragend für Dinge, die mehrmals am selben Tag stattfinden können. Objekt: keyup, touchstart usw. Diese Ereignisse sind für Sie nicht relevant. was vor der Verbindung mit dem Hörer passiert ist. Aber wenn es darum geht, asynchronen Erfolg/Misserfolg haben, sollten Sie etwa Folgendes benötigen:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

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

So etwas ist durch Versprechen möglich, allerdings mit besserer Benennung. Hätten HTML-Bildelemente ein „ready“ die ein Promise zurückgegeben hat, 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 ähneln Versprechen wie Event-Listener, mit folgenden Ausnahmen:

  • Ein Promise kann nur einmal erfolgreich sein oder fehlschlagen. Sie kann nicht zweimal erfolgreich oder scheitern, und umgekehrt.
  • Wenn ein Promise erfolgreich oder fehlgeschlagen ist und du später einen Erfolg/Misserfolg hinzufügst wird der richtige Callback aufgerufen, obwohl das Ereignis früher platziert.

Dies ist bei einem asynchronen Erfolg/Scheitern äußerst nützlich, da Sie weniger interessiert daran, wann genau etwas verfügbar ist, auf das Ergebnis zu reagieren.

Promise-Terminologie

Proof von Domenic Denicola: ersten Entwurf lesen dieses Artikels und bewertete mich mit "F". für Terminologie. Er hat mich inhaftiert, hat mich gezwungen, Zustände und Schicksale und einen besorgten Brief an meine Eltern geschrieben. Trotzdem habe ich die Begriffe verwechseln, aber hier sind die Grundlagen:

Ein Promise kann Folgendes sein:

  • fulfill – Aktion in Bezug auf das Versprechen erfolgreich
  • rejected – Die Aktion in Bezug auf das Versprechen ist fehlgeschlagen.
  • ausstehend – Wurde noch nicht ausgeführt oder abgelehnt
  • settled – erfüllt oder abgelehnt

Technische Daten verwendet den Begriff thenable, um ein Promise-ähnliches Objekt zu beschreiben, da sie eine then-Methode hat. Dieser Begriff erinnert mich an den ehemaligen englischen Football Manager Terry Venables, sodass Ich werde es so wenig wie möglich verwenden.

Promise-Objekte kommen in JavaScript an!

Promise-Objekte gibt es schon seit einiger Zeit in Form von Bibliotheken, z. B.:

Die oben genannten und die JavaScript-Versprechen haben ein gemeinsames, standardisiertes Verhalten. Promises/A+. Wenn jQuery-Nutzer haben eine ähnliche Funktion: Verzögert: Sie können jedoch Verzögerte Anzeigen sind nicht Promise/A+-konform. geringfügig anders und weniger nützlich, also sei vorsichtig! jQuery enthält auch als Promise bezeichnet, und hat die gleichen Probleme.

Obwohl Promise-Implementierungen einem standardisierten Verhalten folgen, APIs insgesamt unterschiedlich sind. JavaScript-Promis sind in der API ähnlich wie RSVP.js. So erstellst du ein Promise:

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 verwendet ein Argument, einen Callback mit zwei Parametern, klären und ablehnen. Tätigt innerhalb des Callbacks eine Aktion, z. B. asynchron, und ruft dann Klären, wenn alles funktioniert hat, andernfalls „Ablehnen“ aufrufen.

Wie bei throw in JavaScript ist es üblich, aber nicht zwingend, mit einem Error-Objekt ablehnen. Fehlerobjekte haben den Vorteil, dass sie Debugging-Tools hilfreicher machen.

So verwendest du dieses Promise:

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 einen erfolgreichen Fall und ein weiteres für den Fall, dass ein Fehler auftritt. Beide sind optional. Sie können also einen Callback für die Erfolg oder Misserfolg.

JavaScript-Versprechen begannen im DOM als "Futures", umbenannt in "Promises", und schließlich in JavaScript verschoben. Wenn sie in JavaScript gespeichert sind, DOM ist besonders nützlich, weil sie auch in nicht browsergestützten JS-Kontexten verfügbar sind, z. B. Node.js (ob sie diese in ihren Kern-APIs verwenden, ist eine andere Frage).

Obwohl es sich um eine JavaScript-Funktion handelt, kann das DOM sie problemlos verwenden. In Alle neuen DOM-APIs mit asynchronen Erfolgs-/Fehlermethoden verwenden Versprechen. Dies geschieht bereits bei Kontingentverwaltung, Font Load-Ereignisse, ServiceWorker Web-MIDI Streams und mehr

Kompatibilität mit anderen Bibliotheken

Die JavaScript Promis API behandelt alles, was eine then()-Methode enthält, wie Versprechen-ähnlich (oder thenable in Seufz), wenn du also eine Bibliothek verwendest ein Q-Versprechen zurückgegeben, kein Problem, es passt gut zum neuen JavaScript verspricht.

Obwohl, wie bereits erwähnt, sind die Deferreds von jQuery ein wenig... nicht hilfreich. Zum Glück kannst du sie wie gewohnt einsetzen, was sich lohnen würde so schnell wie möglich:

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

Hier gibt der $.ajax von jQuery einen Deferred. Da sie eine then()-Methode hat, Promise.resolve() kann es in ein JavaScript-Promise umwandeln. Sie können jedoch Manchmal übergeben deferreds mehrere Argumente an ihre Callbacks. Beispiel:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

JS-Versprechen hingegen ignorieren alle bis auf die erste:

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

Zum Glück ist dies in der Regel das, was Sie wollen oder zumindest Zugriff auf was Sie möchten. Beachten Sie außerdem, dass jQuery nicht der Konvention Fehlerobjekte in Ablehnungen übergeben.

Einfacher komplexer asynchroner Code

Okay, lassen Sie uns ein paar Dinge programmieren. Angenommen, wir möchten:

  1. Rotierendes Ladesymbol starten
  2. Rufen Sie JSON für eine Geschichte ab, das uns den Titel und die URLs für jedes Kapitel liefert.
  3. Titel zur Seite hinzufügen
  4. Jedes Kapitel abrufen
  5. Geschichte zur Seite hinzufügen
  6. Rotierendes Ladesymbol anhalten

...aber informieren Sie den Nutzer auch, wenn auf dem Weg etwas schiefgelaufen ist. Wir benötigen an dieser Stelle anhalten, sonst dreht es sich weiter, und auf einer anderen Benutzeroberfläche stürzen.

Natürlich würden Sie kein JavaScript verwenden, um eine Geschichte zu erzählen, als HTML ausgeliefert wird, schneller ist, aber dieses Muster ist ziemlich häufig beim Umgang mit APIs: Mehrere Daten und etwas tun, wenn alles erledigt ist.

Beginnen wir mit dem Abrufen von Daten aus dem Netzwerk:

Versprechen von XMLHttpRequest

Alte APIs werden aktualisiert, um Promise zu verwenden, wenn dies in einem umgekehrten Zeitraum möglich ist. auf kompatible Weise. XMLHttpRequest ist ein Spitzenkandidaten, 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();
  });
}

Gehen wir nun wie folgt vor:

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

Jetzt können wir HTTP-Anfragen senden, ohne XMLHttpRequest manuell eingeben zu müssen, was gut ist, weniger um das wütende Kamelformat vor XMLHttpRequest zu sehen, desto glücklicher wird mein Leben.

Verkettung

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

Werte transformieren

Sie können Werte einfach transformieren, indem Sie 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
})

Gehen wir als Beispiel aus der Praxis zurück zu:

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

Die Antwort ist im JSON-Format, aber wir empfangen sie derzeit als Nur-Text. Mi. die Funktion get ändern, um die JSON-Datei zu verwenden, responseType, aber wir könnten es auch in Versprechen 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 verwendet und einen transformierten Wert zurückgibt, können wir eine Verknüpfung erstellen:

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

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

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

getJSON() gibt trotzdem ein Promise zurück, das eine URL abruft und dann parst die Antwort im JSON-Format.

Asynchrone Aktionen in die Warteschlange stellen

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

Es ist wie von Zauberhand, wenn du etwas von einem then()-Callback zurückgibst. Wenn Sie einen Wert zurückgeben, wird die nächste then() mit diesem Wert aufgerufen. Sie können jedoch wenn Sie etwas wie ein Versprechen zurückgeben, wartet das nächste then() darauf und wird nur aufgerufen, wenn das Versprechen 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 zu beantragen, fordern wir die erste URL an. Das ist der Zeitpunkt, an dem sich von einfachen Rückrufmustern abzuheben.

Du kannst sogar eine Tastenkombination zum Abrufen von Kapiteln einrichten:

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);
})

story.json wird erst heruntergeladen, wenn getChapter aufgerufen wird, aber der nächste „getChapter“ wird als „wiederverwendetes Story-Versprechen“ bezeichnet, daher: story.json wird nur einmal abgerufen. Geschworen!

Fehlerbehandlung

Wie bereits erwähnt, gibt es für then() zwei Argumente, eines für den Erfolg, eins (oder erfüllen und ablehnen, wie es versprochen wird):

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() hat nichts Besonderes. Es ist nur Zucker für then(undefined, func), ist aber besser lesbar. Beachten Sie, dass die beiden Codes verhalten sich die Beispiele oben nicht gleich. Letzteres entspricht:

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

Der Unterschied ist zwar subtil, aber äußerst nützlich. Abgelehnte Ablehnungen werden übersprungen mit einem Ablehnungs-Callback zur nächsten then() weiterleiten (oder catch(), da gleichwertig). Mit then(func1, func2) werden func1 oder func2 und niemals beides. Aber mit then(func1).catch(func2) sind beide wird aufgerufen, wenn func1 ablehnt, da es sich um separate Schritte in der Kette handelt. Nehmen Folgendes:

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 obige Ablauf ähnelt sehr dem normalen JavaScript-Versuch/-Catch, bei dem Fehler, die während eines „Versuchs“ wechseln Sie direkt zum catch()-Block. Hier ist die (weil ich Flussdiagramme liebe):

Folge den blauen Linien für Versprechungen, die erfüllt werden, die roten Linien für Versprechen, die ablehnen.

JavaScript-Ausnahmen und -Promis

Ablehnungen treten auf, wenn ein Promise explizit, aber auch implizit abgelehnt wird. 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);
})

Es ist also sinnvoll, alle Ihre Promise-bezogenen Arbeiten innerhalb der Promise Konstruktor-Rückruf zurück, sodass Fehler automatisch erkannt und zu Ablehnungen werden.

Dasselbe gilt für Fehler in then()-Callbacks.

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 Geschichte und den Kapiteln können wir einen Catch verwenden, um den Nutzenden einen Fehler anzuzeigen:

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 der Nutzer offline ist), alle folgenden erfolgreichen Callbacks übersprungen, einschließlich des getJSON(), die versucht, die Antwort als JSON zu parsen, und überspringt außerdem den -Callback, der der Seite kapitel1.html hinzufügt. Stattdessen geht es am Fang weiter. Callback des Nutzers an. Deshalb wird die Meldung „Kapitel konnte nicht angezeigt werden“ angezeigt. wird der Seite hinzugefügt, wenn eine der vorherigen Aktionen ist fehlgeschlagen.

Wie bei der Methode „try/catch“ von JavaScript wird der Fehler abgefangen und der fährt fort, sodass das Kreiselsymbol immer ausgeblendet ist, was wir wollen. Die 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'

Sie können catch() nur zu Protokollierungszwecken verwenden, ohne eine Wiederherstellung durchzuführen. aus dem Fehler. Geben Sie dazu den Fehler einfach noch einmal aus. Das könnten wir in Unsere getJSON()-Methode:

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

Wir haben es geschafft, ein Kapitel zu holen, aber wir möchten alle. Lassen Sie uns auftreten können.

Parallelität und Sequenzierung: Das Beste aus beidem erhalten

Asynchron zu denken, ist nicht einfach. Wenn Sie Schwierigkeiten haben, aus dem Weg zu gehen, schreiben Sie den Code so, 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! Er wird jedoch synchronisiert und sperrt den Browser währenddessen. Bis Wenn diese Vorgänge asynchron sind, verwenden wir then(), damit die Dinge nacheinander stattfinden.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: for each url in story.chapterUrls, fetch &amp; 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';
})

Aber wie können wir durch die URLs der Kapitel suchen und sie der Reihe nach abrufen? Dieses 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, unsere Kapitel würden also in beliebiger Reihenfolge angezeigt werden sie herunterladen, wie Pulp Fiction geschrieben wurde. Das ist nicht Pulp Fiction, lösen wir das Problem.

Sequenz erstellen

Wir möchten das Array chapterUrls in eine Folge von Versprechen 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);
  });
})

Zum ersten Mal sehen wir Promise.resolve(), mit dem ein das auf den Wert aufgelöst wird, den Sie ihm geben. Wenn Sie sie bestehen, Instanz von Promise wird sie einfach zurückgegeben (Hinweis:Dies ist ein an die Spezifikation, die für einige Implementierungen noch nicht gilt). Wenn Sie Etwas versprochenes weitergeben (eine then()-Methode hat), erstellt sie eine Echte Promise, die auf die gleiche Weise erfüllt/abgelehnt werden. Bei bestandener Prüfung in einem anderen Wert, z.B. Promise.resolve('Hello') erstellt, wird ein das diesen Wert erfüllt. Wenn Sie es ohne Wert nennen, wie oben beschrieben, erfüllt sie sich mit „nicht definiert“.

Da ist auch Promise.reject(val), das ein Versprechen abgibt, das mit den Wert, den Sie ihr geben (oder nicht definiert).

Wir können den obigen Code mit array.reduce:

// 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())

Es wird derselbe Schritt wie im vorherigen Beispiel ausgeführt, es ist jedoch nicht die separate "Sequenz" . Unser Reduce-Callback wird für jedes Element im Array aufgerufen. "Sequenz" beim ersten Mal Promise.resolve(), aber für den Rest des ruft „Sequenz“ auf was wir vom vorherigen Aufruf zurückgegeben haben. array.reduce ist sehr nützlich, um ein Array auf einen einzigen Wert zu reduzieren, ist ein Versprechen.

Fassen wir nun alles zusammen:

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 da haben wir es, eine vollständig asynchrone Version der Synchronisierungsversion. Aber wir können besser machen. Derzeit wird unsere Seite wie folgt heruntergeladen:

Browser sind ziemlich gut darin, mehrere Inhalte auf einmal herunterzuladen. indem du Kapitel nacheinander herunterlädst. Unser Ziel ist es, alle gleichzeitig herunterladen und verarbeiten, sobald sie alle sind. Zum Glück gibt es dafür eine API:

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

Promise.all nimmt eine Reihe von Versprechen und hinterlässt ein Versprechen, das erfüllt wird wenn sie alle erfolgreich abgeschlossen wurden. Sie erhalten ein Array von Ergebnissen (je nachdem, der Versprechen erfüllt werden) und zwar in derselben Reihenfolge wie die Versprechen, die du gegeben hast.

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 dies Sekunden schneller sein als ein einzelner Ladevorgang. und es ist weniger Code als beim ersten Versuch. Die Kapitel können in einem beliebigen in der richtigen Reihenfolge auf dem Bildschirm erscheinen.

Die wahrgenommene Leistung lässt sich jedoch trotzdem verbessern. Wenn Kapitel eins da ist, um es auf der Seite hinzuzufügen. So können Nutzende mit dem Lesen beginnen, bevor der die Kapitel sind da. Wenn Kapitel 3 kommen, würden wir es nicht da der Nutzer möglicherweise nicht bemerkt, dass Kapitel 2 fehlt. Wenn Kapitel 2 können wir Kapitel 2 und 3 usw. hinzufügen.

Dazu rufen wir JSON für alle unsere Kapitel gleichzeitig ab und erstellen dann ein 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';
})

Und das Beste von beidem! Die Bereitstellung dauert genauso lange, der gesamte Inhalt, aber der Nutzer erhält den ersten Teil des Inhalts früher.

In diesem einfachen Beispiel kommen alle Kapitel zur gleichen Zeit an, aber ist der Vorteil der einzelnen Anzeigen durch mehrere, größere und Kapiteln.

Dazu verwenden Sie Callbacks im Node.js-Stil oder Events in der Nähe den Code verdoppeln, aber, was noch wichtiger ist, nicht so einfach zu befolgen ist. Dieses ist aber noch nicht das Ende der Geschichte für Versprechen, in Kombination mit anderen ES6-Funktionen. wird es sogar noch einfacher.

Zusatzrunde: erweiterte Funktionen

Seit ich diesen Artikel geschrieben habe, können Promise-Objekte jetzt sehr wichtig. Seit Chrome 55 ist es dank asynchroner Funktionen möglich, versprochenen Code als synchron geschrieben, aber ohne den Hauptthread zu blockieren. Sie können Weitere Informationen dazu finden Sie in meinem Artikel zu asynchronen Funktionen. Es gibt Umfassende Unterstützung sowohl für Promise- als auch für asynchrone Funktionen in den wichtigsten Browsern. Einzelheiten hierzu finden Sie in der Versprechen und asynchron Funktion Referenz.

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

Vielen Dank auch an Mathias Bynens für um verschiedene Teile zu aktualisieren des Artikels.