Versprechen vereinfachen verzögerte und asynchrone Berechnungen. Ein Promise stellt einen noch nicht abgeschlossenen Vorgang dar.
Entwickler, bereitet euch auf einen entscheidenden Moment in der Geschichte der Webentwicklung vor.
[Drumroll begins]
Promises sind in JavaScript angekommen!
[Feuerwerk explodiert, glitzerndes Papier regnet von oben, die Menge tobt]
Sie fallen derzeit in eine der folgenden Kategorien:
- Die Leute um Sie herum jubeln, aber Sie sind sich nicht sicher, was das ganze Aufhebens soll. Vielleicht sind Sie sich nicht einmal sicher, was ein „Versprechen“ ist. Sie würden die Schultern zucken, aber das Gewicht des glitzernden Papiers lastet auf Ihren Schultern. Keine Sorge, es hat auch bei mir ewig gedauert, bis ich herausgefunden habe, warum ich mich um diese Dinge kümmern sollte. Vielleicht möchten Sie von vorn beginnen.
- Sie schlagen in die Luft. Das wurde auch Zeit, oder? Sie haben diese Promise-Dinge schon einmal verwendet, aber es stört Sie, dass alle Implementierungen eine etwas andere API haben. Welche API wird für die offizielle JavaScript-Version verwendet? Am besten beginnen Sie mit der Terminologie.
- Sie wussten das bereits und lachen über diejenigen, die herumspringen, als wäre es eine Neuigkeit für sie. Genießen Sie einen Moment lang Ihr Gefühl der Überlegenheit und rufen Sie dann die API-Referenz auf.
Browserunterstützung und Polyfill
Wenn Sie Browser, die keine vollständige Implementierung von Promises haben, an die Spezifikation anpassen oder Promises anderen Browsern und Node.js hinzufügen möchten, sehen Sie sich die Polyfill-Version (2 KB, komprimiert) an.
Aber was hat es damit eigentlich auf sich?
JavaScript ist ein einzelner Thread, d. h. zwei Script-Abschnitte können nicht gleichzeitig ausgeführt werden, sondern müssen nacheinander ausgeführt werden. In Browsern teilt sich JavaScript einen Thread mit vielen anderen Elementen, die sich von Browser zu Browser unterscheiden. Normalerweise befindet sich JavaScript jedoch in derselben Warteschlange wie das Zeichnen, das Aktualisieren von Stilen und die Verarbeitung von Nutzeraktionen wie das Hervorheben von Text und die Interaktion mit Formularelementen. Aktivitäten in einem dieser Bereiche verzögern die anderen.
Als Mensch sind Sie multi-threaded. Sie können Text mit mehreren Fingern tippen und eine Unterhaltung gleichzeitig führen. Die einzige Blockierungsfunktion, mit der wir umgehen müssen, ist das Niesen. Dabei müssen alle aktuellen Aktivitäten für die Dauer des Niesens pausiert werden. Das ist ziemlich ärgerlich, vor allem, wenn Sie fahren und ein Gespräch führen möchten. Sie möchten keinen Code schreiben, der schnupft.
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 überhaupt kein Problem. Wir rufen das Bild ab, fügen einige Listener hinzu und JavaScript kann die Ausführung beenden, bis einer dieser Listener aufgerufen wird.
Leider ist es im Beispiel oben möglich, dass die Ereignisse bereits stattgefunden haben, bevor wir damit begonnen haben, darauf zu warten. Daher müssen wir das Problem mithilfe 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 mit Fehlern werden nicht erkannt, bevor wir sie überwachen konnten. Leider bietet uns das DOM dafür keine Möglichkeit. Dabei wird ein Bild geladen. Noch komplizierter 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 hervorragend für Dinge, die mehrmals am selben Objekt auftreten können, z. B. keyup
oder touchstart
. Bei diesen Ereignissen spielt es keine Rolle, was vor dem Anhängen des Listeners passiert ist. Bei asynchronen Erfolgen/Fehlern sollte es aber idealerweise so aussehen:
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. Wenn HTML-Bildelemente eine „ready“-Methode hätten, die ein Versprechen 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 genommen sind Versprechen ein bisschen wie Ereignis-Listener, mit folgenden Ausnahmen:
- Ein Versprechen kann nur einmal erfolgreich sein oder fehlschlagen. Sie kann nicht zweimal erfolgreich sein oder fehlschlagen. Es ist auch nicht möglich, von Erfolg zu Misserfolg zu wechseln oder umgekehrt.
- Wenn ein Versprechen erfolgreich war oder fehlgeschlagen ist und Sie später einen Erfolgs-/Fehler-Callback hinzufügen, wird der richtige Callback aufgerufen, auch wenn das Ereignis früher stattgefunden hat.
Dies ist bei asynchronen Erfolgen/Fehlern äußerst nützlich, da Sie weniger daran interessiert sind, wann genau etwas verfügbar wurde, und mehr daran interessiert sind, auf das Ergebnis zu reagieren.
Terminologie für Promise
Domenic Denicola hat den ersten Entwurf dieses Artikels Korrektur gelesen und mir eine „F“ für die Terminologie gegeben. Er setzte mich in den Arrest, zwang mich, Staaten und Schicksale 100 Mal abzuschreiben, und schrieb meinen Eltern einen besorgten Brief. Trotzdem verwechsele ich immer noch viele Begriffe. Hier sind die Grundlagen:
Ein Versprechen kann:
- fulfilled: Die Aktion im Zusammenhang mit dem Versprechen war erfolgreich.
- rejected: Die Aktion im Zusammenhang mit dem Versprechen ist fehlgeschlagen.
- ausstehend: Die Anfrage wurde noch nicht erfüllt oder abgelehnt.
- settled: Erfüllt oder abgelehnt
In der Spezifikation wird der Begriff thenable verwendet, um ein Objekt zu beschreiben, das einem Versprechen ähnelt, da es eine then
-Methode hat. Dieser Begriff erinnert mich an den ehemaligen englischen Fußballmanager Terry Venables. Deshalb werde ich ihn so wenig wie möglich verwenden.
Promises in JavaScript
Promise-Objekte gibt es schon seit einiger Zeit in Form von Bibliotheken, z. B.:
Die oben und die JavaScript-Versprechen haben ein gemeinsames, standardisiertes Verhalten namens Promises/A+. Wenn Sie jQuery verwenden, gibt es eine ähnliche Funktion namens Deferreds. Deferreds sind jedoch nicht Promise/A+-kompatibel, was sie etwas anders und weniger nützlich macht. jQuery hat auch einen Promise-Typ, der aber nur eine Teilmenge von Deferred ist und dieselben Probleme aufweist.
Obwohl Promise-Implementierungen einem standardisierten Verhalten folgen, unterscheiden sich die APIs insgesamt. JavaScript-Promises ähneln in der API RSVP.js. So erstellst du 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 nimmt ein Argument an, einen Callback mit zwei Parametern: „resolve“ und „reject“. Führe im Callback etwas aus, z. B. asynchron, und rufe dann „resolve“ auf, wenn alles funktioniert hat, andernfalls „reject“.
Wie bei throw
in Plain Old JavaScript ist es üblich, aber nicht erforderlich, mit einem Fehlerobjekt abzulehnen. Der Vorteil von Fehlerobjekten besteht darin, dass sie einen Stack-Trace erfassen, wodurch die Debugging-Tools hilfreicher werden.
So verwendest du dieses Promise:
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
then()
nimmt zwei Argumente an, einen Callback für den Erfolgsfall und einen weiteren für den Fehlerfall. Beides ist optional. Sie können also einen Callback nur für den Fall des Erfolgs oder des Fehlers hinzufügen.
JavaScript-Versprechen wurden im DOM als „Futures“ eingeführt, in „Promises“ umbenannt und schließlich in JavaScript verschoben. Es ist großartig, sie in JavaScript statt im DOM zu haben, da sie in JS-Kontexten außerhalb des Browsers wie Node.js verfügbar sind. Ob sie in ihren Kern-APIs verwendet werden, ist eine andere Frage.
Obwohl sie eine JavaScript-Funktion sind, werden sie im DOM verwendet. Tatsächlich werden alle neuen DOM-APIs mit asynchronen Erfolg-/Fehlermethoden Promises verwenden. Dies ist bereits bei Kontingentverwaltung, Font Load-Ereignissen, ServiceWorker, Web MIDI, Streams und mehr der Fall.
Kompatibilität mit anderen Bibliotheken
Die JavaScript Promises API behandelt alles mit einer then()
-Methode als Promise-Objekt (oder thenable
in Promise-Sprache sigh). Wenn Sie also eine Bibliothek verwenden, die ein Q-Promise zurückgibt, ist das in Ordnung. Sie funktioniert mit den neuen JavaScript-Promises.
Obwohl, wie bereits erwähnt, sind die Deferreds von jQuery ein wenig... nicht hilfreich. Glücklicherweise können Sie sie in Standardversprechen umwandeln. Das sollten Sie so bald wie möglich tun:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
Hier gibt die $.ajax
von jQuery ein Deferred zurück. Da es eine then()
-Methode hat, kann Promise.resolve()
daraus ein JavaScript-Promise erstellen. Manchmal übergeben Deffereds jedoch mehrere Argumente an ihre Rückrufe, z. B.:
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) {
// ...
})
Glücklicherweise ist dies in der Regel das, was Sie wollen, oder bietet Ihnen zumindest Zugriff auf das, was Sie wollen. Beachten Sie außerdem, dass jQuery nicht der Konvention folgt, Fehlerobjekte an Ablehnungen weiterzugeben.
Komplexer asynchroner Code vereinfacht
Okay, dann programmieren wir etwas. Nehmen wir an, wir möchten:
- Ein Ladesymbol anzeigen, um das Laden anzuzeigen
- JSON für eine Geschichte abrufen, die den Titel und die URLs für jedes Kapitel enthält
- Fügen Sie der Seite einen Titel hinzu.
- Jedes Kapitel abrufen
- Geschichte zur Seite hinzufügen
- Rotierendes Ladesymbol anhalten
… aber informieren Sie den Nutzer auch, wenn etwas schiefgelaufen ist. An dieser Stelle sollten wir auch den Spinner anhalten, da er sonst weiter rotiert, sich überschlägt und mit einer anderen Benutzeroberfläche kollidiert.
Natürlich würden Sie kein JavaScript verwenden, um eine Story bereitzustellen, die Bereitstellung als HTML ist schneller, aber dieses Muster ist bei der Arbeit mit APIs recht üblich: Mehrere Datenabrufe und dann eine Aktion, wenn alles erledigt ist.
Zuerst geht es um das Abrufen von Daten aus dem Netzwerk:
XMLHttpRequest als Promise verwenden
Alte APIs werden auf die Verwendung von Promises umgestellt, sofern dies abwärtskompatibel möglich ist. XMLHttpRequest
ist ein geeigneter Kandidat, aber in der Zwischenzeit schreiben wir eine einfache Funktion, um eine GET-Anfrage zu senden:
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();
});
}
Jetzt können wir es verwenden:
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 großartig, denn je weniger ich das irritierende Camel-Case-Format von XMLHttpRequest
sehen muss, desto glücklicher bin ich.
Verkettung
then()
ist aber nicht das Ende der Geschichte. Sie können then
s miteinander verknüpfen, um Werte zu transformieren oder zusätzliche asynchrone Aktionen nacheinander auszufü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
})
Als praktisches Beispiel gehen wir zurück zu:
get('story.json').then(function(response) {
console.log("Success!", response);
})
Die Antwort ist JSON, wird aber derzeit als Nur-Text empfangen. Wir könnten die get-Funktion so ändern, dass die JSON-responseType
verwendet wird, aber wir könnten es auch im Promise-Land 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 annimmt 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önnen 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 die Antwort als JSON parst.
Asynchrone Aktionen in die Warteschlange stellen
Sie können then
auch verketten, um asynchrone Aktionen nacheinander auszuführen.
Wenn du etwas von einem then()
-Callback zurückgibst, ist das ein bisschen magisch.
Wenn Sie einen Wert zurückgeben, wird die nächste then()
mit diesem Wert aufgerufen. Wenn Sie jedoch etwas Promise-ähnliches zurückgeben, wartet der nächste then()
darauf und wird nur aufgerufen, wenn dieses Promise erledigt ist (Erfolg/Fehlschlag). 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 zur Verfügung stellt, die angefragt werden sollen, und dann die erste davon. Hier heben sich Versprechen wirklich von einfachen Rückrufmustern ab.
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. Bei den nächsten Aufrufen von getChapter
wird das Story-Versprechen jedoch wiederverwendet, sodass story.json
nur einmal abgerufen wird. Yay Promises!
Fehlerbehandlung
Wie bereits erwähnt, nimmt then()
zwei Argumente an, eines für den Erfolg und eines für den Fehler (oder „erfüllen“ und „ablehnen“ in der Sprache von Versprechen):
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 an catch()
, es ist nur Zucker für then(undefined, func)
, ist aber besser lesbar. Die beiden Codebeispiele oben verhalten sich nicht gleich. Das letzte Beispiel 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. Bei einer Promise-Ablehnung wird mit einem Ablehnungs-Callback zum nächsten then()
(oder catch()
, da dies äquivalent ist) gesprungen. Bei then(func1, func2)
wird func1
oder func2
aufgerufen, niemals beides. Bei then(func1).catch(func2)
werden jedoch beide aufgerufen, wenn func1
abgelehnt wird, da es sich um separate Schritte in der Kette handelt. Beachten Sie 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/Catching. Fehler, die innerhalb eines „try“-Vorgangs auftreten, werden sofort an den catch()
-Block gesendet. Hier ist das Ganze als Flussdiagramm (weil ich Flussdiagramme liebe):
Folgen Sie den blauen Linien für erfüllte Versprechen oder den roten für abgelehnte Versprechen.
JavaScript-Ausnahmen und ‑Versprechen
Ablehnungen treten auf, wenn ein Versprechen explizit abgelehnt wird, aber auch implizit, wenn im Konstruktor-Callback ein Fehler geworfen 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 mit Promises zusammenhängenden Aufgaben im Rückruf des Promise-Konstruktors auszuführen, damit Fehler automatisch erkannt und abgelehnt werden.
Dasselbe gilt für Fehler, die in then()
-Callbacks auftreten.
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 der Nutzer ist offline), werden alle nachfolgenden Erfolgs-Callbacks übersprungen, einschließlich des Callbacks in getJSON()
, der versucht, die Antwort als JSON zu parsen. Außerdem wird der Callback übersprungen, der der Seite „chapter1.html“ hinzufügt. Stattdessen wird der Rückgabewert an den catch-Callback übergeben. Daher wird der Seite „Fehler beim Anzeigen des Kapitels“ hinzugefügt, wenn eine der vorherigen Aktionen fehlgeschlagen ist.
Ähnlich wie bei JavaScript-Try/Catch wird der Fehler abgefangen und der nachfolgende Code wird fortgesetzt. Das bedeutet, dass das Ladesymbol immer ausgeblendet ist, was wir auch möchten. Das obige Beispiel 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 einfach noch einmal werfen. Das könnten 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 benötigen alle. Lassen Sie uns das machen.
Parallelisierung und Sequenzierung: Das Beste aus beiden Welten
Es ist nicht einfach, asynchron zu denken. Wenn Sie nicht weiterkommen, 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! Aber es synchronisiert und sperrt den Browser, während Dinge heruntergeladen werden. Damit dies asynchron funktioniert, verwenden wir then()
, damit die Aktionen nacheinander ausgeführt werden.
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';
})
Aber wie können wir die Kapitel-URLs in einer Schleife durchgehen und sie der Reihe nach abrufen? Dies 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 werden unsere Kapitel in der Reihenfolge angezeigt, in der sie heruntergeladen werden. So wurde im Grunde Pulp Fiction geschrieben. Das ist kein Pulp Fiction, also lass uns das beheben.
Sequenz erstellen
Wir möchten unser chapterUrls
-Array in eine Sequenz 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);
});
})
Dies ist das erste Mal, dass wir Promise.resolve()
sehen, das ein Versprechen abbildet, das auf den von dir angegebenen Wert aufgelöst wird. Wenn Sie eine Instanz von Promise
übergeben, wird sie einfach zurückgegeben. Hinweis: Dies ist eine Änderung an der Spezifikation, die einige Implementierungen noch nicht einhalten. Wenn Sie ihm etwas Versprechendes übergeben (mit einer then()
-Methode), wird eine echte Promise
erstellt, die auf dieselbe Weise erfüllt oder abgelehnt wird. Wenn Sie einen anderen Wert übergeben, z. B. Promise.resolve('Hello')
, wird ein Versprechen erstellt, das mit diesem Wert erfüllt wird. Wenn Sie sie wie oben ohne Wert nennen, erfüllt sie sich mit „nicht definiert“.
Es gibt auch Promise.reject(val)
, mit dem ein Versprechen erstellt wird, das mit dem angegebenen Wert (oder „undefiniert“) abgelehnt wird.
Mit array.reduce
können wir den Code oben aufräumen:
// 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())
Dies entspricht dem vorherigen Beispiel, erfordert aber keine separate Variable „sequence“. Der Rückruf für „reduce“ wird für jedes Element im Array aufgerufen.
„sequence“ ist beim ersten Mal Promise.resolve()
, aber bei den restlichen Aufrufen ist „sequence“ das, was wir vom vorherigen Aufruf zurückgegeben haben. array.reduce
ist sehr nützlich, um ein Array auf einen einzelnen Wert zurückzuführen, in diesem Fall ein Versprechen.
Zusammenfassend:
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 ist es, eine vollständig asynchrone Version der Synchronisationsversion. Aber wir können noch besser werden. Derzeit wird unsere Seite so heruntergeladen:
Browser sind ziemlich gut darin, mehrere Inhalte auf einmal herunterzuladen, sodass die Leistung beeinträchtigt wird, wenn Kapitel nacheinander heruntergeladen werden. Wir möchten sie alle gleichzeitig herunterladen und dann verarbeiten, sobald sie alle angekommen sind. Glücklicherweise gibt es dafür eine API:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
})
Promise.all
nimmt eine Reihe von Versprechen entgegen und erstellt ein Versprechen, das erfüllt wird, wenn alle erfolgreich abgeschlossen wurden. Du erhältst eine Reihe von Ergebnissen (je nachdem, was du versprichst) in der Reihenfolge, in der du deine Versprechen 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 das Sekunden schneller gehen als das Laden einzeln. Außerdem ist es weniger Code 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 noch verbessern. Wenn Kapitel 1 da ist, sollten wir es der Seite hinzufügen. So kann der Nutzer mit dem Lesen beginnen, bevor der Rest der Kapitel verfügbar ist. Wenn Kapitel 3 fertig ist, fügen wir es der Seite nicht hinzu, da der Nutzer möglicherweise nicht merkt, dass Kapitel 2 fehlt. Wenn Kapitel 2 erscheint, können wir Kapitel 2 und 3 usw. hinzufügen.
Dazu rufen wir JSON für alle Kapitel gleichzeitig 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, das war's auch schon. Es dauert genauso lange, bis alle Inhalte gesendet werden, aber der Nutzer erhält die ersten Inhalte früher.
In diesem einfachen Beispiel werden alle Kapitel ungefähr zur selben Zeit empfangen. Bei mehr und größeren Kapiteln ist der Vorteil, sie einzeln anzuzeigen, jedoch noch größer.
Wenn Sie dafür Callbacks oder Ereignisse im Node.js-Stil verwenden, ist der Code doppelt so groß, was noch wichtiger ist, aber nicht so einfach nachzuvollziehen. Das ist jedoch noch nicht alles, was es über Versprechen zu sagen gibt. In Kombination mit anderen ES6-Funktionen sind sie noch einfacher.
Bonusrunde: Erweiterte Funktionen
Seit ich diesen Artikel ursprünglich geschrieben habe, hat sich die Möglichkeit, Promises zu verwenden, stark erweitert. Seit Chrome 55 ist es mit asynchronen Funktionen möglich, Promise-basierten Code so zu schreiben, als wäre er synchron, aber ohne den Hauptthread zu blockieren. Weitere Informationen dazu findest du in meinem Artikel zu asynchronen Funktionen. Sowohl Promise- als auch asynchrone Funktionen werden von den wichtigsten Browsern unterstützt. Weitere Informationen finden Sie in den MDN-Referenzen zu Promises und async-Funktionen.
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 vorgenommen haben.
Außerdem möchten wir Mathias Bynens für die Aktualisierung verschiedener Teile des Artikels danken.