Stellen Sie sich vor, Sie sind ein Top-Sänger und die Fans fragen Tag und Nacht nach Ihrem kommenden Song.
Um Abhilfe zu schaffen, versprechen Sie, es ihnen nach der Veröffentlichung zuzusenden. Du gibst deinen Fans eine Liste. Sie können ihre E-Mail-Adressen eingeben, sodass alle Abonnenten den Song sofort erhalten, sobald er verfügbar ist. Und selbst wenn etwas sehr schief geht, zum Beispiel ein Brand im Studio, sodass Sie den Song nicht veröffentlichen können, werden sie trotzdem benachrichtigt.
Alle sind glücklich: Du, weil die Leute Dich nicht mehr drängen, und die Fans, weil sie den Song nicht verpassen werden.
Dies ist eine reale Analogie für Dinge, die wir beim Programmieren oft haben:
Ein „produzierender Code“, der etwas tut und Zeit braucht. Zum Beispiel ein Code, der die Daten über ein Netzwerk lädt. Das ist ein „Sänger“.
Ein „konsumierender Code“, der das Ergebnis des „produzierenden Codes“ haben möchte, sobald dieser fertig ist. Viele Funktionen benötigen möglicherweise dieses Ergebnis. Das sind die „Fans“.
Ein Versprechen ist ein spezielles JavaScript-Objekt, das den „produzierenden Code“ und den „konsumierenden Code“ miteinander verknüpft. In Bezug auf unsere Analogie: Dies ist die „Abonnementliste“. Der „produzierende Code“ benötigt die Zeit, die er benötigt, um das versprochene Ergebnis zu erzielen, und das „Versprechen“ stellt dieses Ergebnis allen abonnierten Codes zur Verfügung, wenn es fertig ist.
Die Analogie ist nicht besonders zutreffend, da JavaScript-Versprechen komplexer sind als eine einfache Abonnementliste: Sie weisen zusätzliche Funktionen und Einschränkungen auf. Aber für den Anfang ist es in Ordnung.
Die Konstruktorsyntax für ein Promise-Objekt lautet:
let versprochen = neues Versprechen(Funktion(auflösen, ablehnen) { // Executor (der produzierende Code, „Sänger“) });
Die an new Promise
übergebene Funktion wird Executor genannt. Wenn new Promise
erstellt wird, wird der Executor automatisch ausgeführt. Es enthält den erzeugenden Code, der letztendlich das Ergebnis erzeugen soll. Im Sinne der obigen Analogie: Der Testamentsvollstrecker ist der „Sänger“.
Seine Argumente resolve
und reject
sind Rückrufe, die von JavaScript selbst bereitgestellt werden. Unser Code befindet sich nur im Executor.
Wenn der Ausführende das Ergebnis erhält, egal ob früh oder spät, sollte er einen dieser Rückrufe aufrufen:
resolve(value)
– wenn der Job erfolgreich abgeschlossen wurde, mit value
.
reject(error)
– wenn ein Fehler aufgetreten ist, ist error
das Fehlerobjekt.
Zusammenfassend lässt sich sagen: Der Executor läuft automatisch und versucht, einen Job auszuführen. Wenn der Versuch abgeschlossen ist, ruft er resolve
auf, wenn er erfolgreich war, bzw. reject
wenn ein Fehler aufgetreten ist.
Das vom new Promise
-Konstruktor zurückgegebene promise
Objekt verfügt über die folgenden internen Eigenschaften:
state
– zunächst "pending"
, ändert sich dann entweder zu "fulfilled"
, wenn resolve
aufgerufen wird, oder zu "rejected"
, wenn reject
“ aufgerufen wird.
result
– zunächst undefined
, ändert sich dann zu value
, wenn resolve(value)
aufgerufen wird, oder error
, wenn reject(error)
aufgerufen wird.
Daher verschiebt der Testamentsvollstrecker promise
schließlich in einen dieser Staaten:
Später werden wir sehen, wie „Fans“ diese Änderungen abonnieren können.
Hier ist ein Beispiel für einen Promise-Konstruktor und eine einfache Executor-Funktion mit „Code erzeugen“, der Zeit braucht (über setTimeout
):
let versprochen = neues Versprechen(Funktion(auflösen, ablehnen) { // Die Funktion wird automatisch ausgeführt, wenn das Versprechen erstellt wird // nach 1 Sekunde signalisieren, dass die Arbeit erledigt ist mit dem Ergebnis „done“ setTimeout(() => discover("done"), 1000); });
Wir können zwei Dinge sehen, wenn wir den obigen Code ausführen:
Der Executor wird automatisch und sofort aufgerufen (durch new Promise
).
Der Testamentsvollstrecker erhält zwei Argumente: resolve
und reject
. Diese Funktionen sind von der JavaScript-Engine vordefiniert, sodass wir sie nicht erstellen müssen. Wir sollten einen von ihnen erst anrufen, wenn wir bereit sind.
Nach einer Sekunde der „Verarbeitung“ ruft der Executor resolve("done")
auf, um das Ergebnis zu erzeugen. Dadurch ändert sich der Status des promise
-Objekts:
Das war ein Beispiel für einen erfolgreichen Auftragsabschluss, ein „erfülltes Versprechen“.
Und nun ein Beispiel dafür, wie der Testamentsvollstrecker das Versprechen mit einem Fehler ablehnt:
let versprochen = neues Versprechen(Funktion(auflösen, ablehnen) { // nach 1 Sekunde signalisieren, dass der Job mit einem Fehler beendet wurde setTimeout(() => Reject(new Error("Whoops!")), 1000); });
Der Aufruf von reject(...)
verschiebt das Promise-Objekt in den Zustand "rejected"
:
Zusammenfassend lässt sich sagen, dass der Ausführende einen Job ausführen sollte (normalerweise etwas, das Zeit braucht) und dann resolve
oder reject
aufrufen sollte, um den Status des entsprechenden Promise-Objekts zu ändern.
Eine Zusage, die entweder gelöst oder abgelehnt wird, wird als „erledigt“ bezeichnet, im Gegensatz zu einer zunächst „ausstehenden“ Zusage.
Es kann nur ein einzelnes Ergebnis oder ein Fehler vorliegen
Der Testamentsvollstrecker sollte nur eine resolve
oder eine reject
aufrufen. Jede Zustandsänderung ist endgültig.
Alle weiteren Aufrufe von resolve
und reject
werden ignoriert:
let versprochen = neues Versprechen(Funktion(auflösen, ablehnen) { lösen("erledigt"); ablehnen(neuer Fehler("…")); // ignoriert setTimeout(() => discover("…")); // ignoriert });
Die Idee ist, dass eine vom Ausführenden ausgeführte Aufgabe möglicherweise nur ein Ergebnis oder einen Fehler hat.
Außerdem wird resolve
/ reject
nur ein Argument (oder keins) erwartet und zusätzliche Argumente ignoriert.
Mit Error
ablehnen
Falls etwas schief geht, sollte der Testamentsvollstrecker reject
aufrufen. Dies kann mit jeder Art von Argument erfolgen (genau wie resolve
). Es wird jedoch empfohlen, Error
Objekte (oder Objekte, die von Error
erben) zu verwenden. Die Gründe dafür werden bald klar werden.
Rufen Sie sofort resolve
/ reject
auf
In der Praxis macht ein Executor normalerweise etwas asynchron und ruft nach einiger Zeit resolve
/ reject
auf, aber das muss nicht sein. Wir können auch sofort resolve
oder reject
aufrufen, etwa so:
let versprochen = neues Versprechen(Funktion(auflösen, ablehnen) { // Wir nehmen uns nicht die Zeit, die Arbeit zu erledigen lösen(123); // sofort das Ergebnis ausgeben: 123 });
Dies kann beispielsweise passieren, wenn wir mit der Ausführung einer Aufgabe beginnen, dann aber feststellen, dass alles bereits abgeschlossen und zwischengespeichert ist.
Das ist in Ordnung. Wir haben sofort ein gelöstes Versprechen.
Der state
und result
sind intern
Die Eigenschaften state
und result
des Promise-Objekts sind intern. Wir können nicht direkt darauf zugreifen. Dafür können wir die Methoden .then
/ .catch
/ .finally
nutzen. Sie werden im Folgenden beschrieben.
Ein Promise-Objekt dient als Verbindung zwischen dem Executor (dem „produzierenden Code“ oder „Sänger“) und den konsumierenden Funktionen (den „Fans“), die das Ergebnis oder den Fehler erhalten. Konsumierende Funktionen können mit den Methoden .then
und .catch
registriert (abonniert) werden.
Das wichtigste und grundlegendste ist .then
.
Die Syntax lautet:
versprechen.then( function(result) { /* ein erfolgreiches Ergebnis verarbeiten */ }, function(error) { /* einen Fehler behandeln */ } );
Das erste Argument von .then
ist eine Funktion, die ausgeführt wird, wenn das Versprechen aufgelöst wird, und das Ergebnis empfängt.
Das zweite Argument von .then
ist eine Funktion, die ausgeführt wird, wenn das Versprechen abgelehnt wird und den Fehler empfängt.
Hier ist zum Beispiel eine Reaktion auf ein erfolgreich gelöstes Versprechen:
let versprochen = neues Versprechen(Funktion(auflösen, ablehnen) { setTimeout(() => discover("done!"), 1000); }); // „resolve“ führt die erste Funktion in .then aus versprechen.then( Ergebnis => Warnung(Ergebnis), // zeigt „fertig!“ an nach 1 Sekunde error => alarm(error) // wird nicht ausgeführt );
Die erste Funktion wurde ausgeführt.
Und im Falle einer Absage die zweite:
let versprochen = neues Versprechen(Funktion(auflösen, ablehnen) { setTimeout(() => Reject(new Error("Whoops!")), 1000); }); // Reject führt die zweite Funktion in .then aus versprechen.then( result => warning(result), // wird nicht ausgeführt error => alarm(error) // zeigt „Fehler: Whoops!“ nach 1 Sekunde );
Wenn wir nur an erfolgreichen Abschlüssen interessiert sind, können wir .then
nur ein Funktionsargument angeben:
let Promise = new Promise(resolve => { setTimeout(() => discover("done!"), 1000); }); versprechen.then(alert); // zeigt „fertig!“ nach 1 Sekunde
Wenn wir nur an Fehlern interessiert sind, können wir null
als erstes Argument verwenden: .then(null, errorHandlingFunction)
. Oder wir können .catch(errorHandlingFunction)
verwenden, was genau das Gleiche ist:
let Promise = new Promise((lösen, ablehnen) => { setTimeout(() => Reject(new Error("Whoops!")), 1000); }); // .catch(f) ist dasselbe wie Promise.then(null, f) versprechen.catch(alert); // zeigt „Fehler: Hoppla!“ nach 1 Sekunde
Der Aufruf .catch(f)
ist ein vollständiges Analogon von .then(null, f)
, es ist nur eine Abkürzung.
Genauso wie es in einem regulären try {...} catch {...}
eine finally
Klausel gibt, gibt es auch in Promises finally
eine Klausel.
Der Aufruf .finally(f)
ähnelt .then(f, f)
in dem Sinne, dass f
immer ausgeführt wird, wenn das Versprechen erfüllt ist: sei es auflösen oder ablehnen.
Die Idee von finally
besteht darin, einen Handler einzurichten, der die Bereinigung/Finalisierung durchführt, nachdem die vorherigen Vorgänge abgeschlossen sind.
B. das Stoppen von Ladeanzeigen, das Schließen nicht mehr benötigter Verbindungen usw.
Betrachten Sie es als Party-Finish. Ganz gleich, ob eine Party gut oder schlecht war und wie viele Freunde dabei waren, wir müssen (oder sollten zumindest) danach immer noch aufräumen.
Der Code könnte so aussehen:
neues Versprechen((auflösen, ablehnen) => { /* etwas tun, das Zeit braucht, und dann „resolve“ oder „reject“ aufrufen */ }) // wird ausgeführt, wenn das Versprechen erfüllt ist, egal ob erfolgreich oder nicht .finally(() => Ladestoppanzeige) // also wird die Ladeanzeige immer gestoppt, bevor wir fortfahren .then(result => Ergebnis anzeigen, err => Fehler anzeigen)
Bitte beachten Sie jedoch, dass finally(f)
nicht unbedingt ein Alias von then(f,f)
ist.
Es gibt wichtige Unterschiede:
Ein finally
“-Handler hat keine Argumente. finally
wissen wir nicht, ob das Versprechen erfolgreich ist oder nicht. Das ist in Ordnung, da unsere Aufgabe normalerweise darin besteht, „allgemeine“ Finalisierungsverfahren durchzuführen.
Schauen Sie sich bitte das obige Beispiel an: Wie Sie sehen, hat der finally
Handler keine Argumente und das Promise-Ergebnis wird vom Next-Handler verarbeitet.
Ein finally
“-Handler „reicht“ das Ergebnis oder den Fehler an den nächsten geeigneten Handler weiter.
Hier wird beispielsweise das Ergebnis finally
an then
weitergegeben:
neues Versprechen((auflösen, ablehnen) => { setTimeout(() => discover("value"), 2000); }) .finally(() => alarm("Promise ready")) // wird zuerst ausgelöst .then(result => warning(result)); // <-- .then zeigt „Wert“
Wie Sie sehen können, wird der vom ersten Versprechen zurückgegebene value
finally
an das nächste then
weitergegeben.
Das ist sehr praktisch, da finally
nicht dazu gedacht ist, ein versprochenes Ergebnis zu verarbeiten. Wie gesagt, es ist ein Ort, an dem allgemeine Aufräumarbeiten durchgeführt werden können, unabhängig vom Ergebnis.
Und hier ist ein Beispiel für einen Fehler, damit wir sehen können, wie er finally
durchgereicht wird, um catch
:
neues Versprechen((auflösen, ablehnen) => { throw new Error("error"); }) .finally(() => alarm("Promise ready")) // wird zuerst ausgelöst .catch(err => alarm(err)); // <-- .catch zeigt den Fehler an
Ein finally
“-Handler sollte auch nichts zurückgeben. Wenn dies der Fall ist, wird der zurückgegebene Wert stillschweigend ignoriert.
Die einzige Ausnahme von dieser Regel besteht, wenn ein finally
“-Handler einen Fehler auslöst. Dann geht dieser Fehler an den nächsten Handler und nicht an ein vorheriges Ergebnis.
Zusammenfassend:
Ein finally
-Handler erhält nicht das Ergebnis des vorherigen Handlers (er hat keine Argumente). Dieses Ergebnis wird stattdessen an den nächsten geeigneten Handler weitergeleitet.
Wenn ein finally
Handler etwas zurückgibt, wird es ignoriert.
Wenn finally
ein Fehler ausgegeben wird, geht die Ausführung zum nächsten Fehlerbehandler.
Diese Funktionen sind hilfreich und sorgen dafür, dass die Dinge genau richtig funktionieren, wenn wir sie finally
so verwenden, wie sie verwendet werden sollen: für allgemeine Bereinigungsverfahren.
Wir können Handler an vereinbarte Zusagen anhängen
Wenn ein Versprechen aussteht, warten .then/catch/finally
-Handler auf das Ergebnis.
Manchmal kann es sein, dass ein Versprechen bereits erfüllt ist, wenn wir ihm einen Handler hinzufügen.
In einem solchen Fall werden diese Handler einfach sofort ausgeführt:
// Das Versprechen wird sofort nach der Erstellung eingelöst let Promise = new Promise(resolve => discover("done!")); versprechen.then(alert); // Erledigt! (erscheint gerade)
Beachten Sie, dass Versprechen dadurch wirkungsvoller sind als das reale „Abonnementlisten“-Szenario. Wenn der Sänger sein Lied bereits veröffentlicht hat und sich dann jemand in die Abonnementliste einträgt, wird er dieses Lied wahrscheinlich nicht erhalten. Abonnements im echten Leben müssen vor der Veranstaltung abgeschlossen werden.
Versprechen sind flexibler. Wir können jederzeit Handler hinzufügen: Wenn das Ergebnis bereits vorhanden ist, werden sie einfach ausgeführt.
Als Nächstes sehen wir uns weitere praktische Beispiele dafür an, wie Versprechen uns beim Schreiben von asynchronem Code helfen können.
Wir haben die Funktion loadScript
zum Laden eines Skripts aus dem vorherigen Kapitel.
Hier ist die Callback-basierte Variante, nur um uns daran zu erinnern:
Funktion LoadScript(src, Callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(null, script); script.onerror = () => callback(new Error(`Skriptladefehler für ${src}`)); document.head.append(script); }
Schreiben wir es mit Promises um.
Für die neue Funktion loadScript
ist kein Rückruf erforderlich. Stattdessen wird ein Promise-Objekt erstellt und zurückgegeben, das aufgelöst wird, wenn der Ladevorgang abgeschlossen ist. Der äußere Code kann mithilfe von .then
Handler (abonnierende Funktionen) hinzufügen:
Funktion LoadScript(src) { neues Versprechen zurückgeben(Funktion(auflösen, ablehnen) { let script = document.createElement('script'); script.src = src; script.onload = () => Auflösung(Skript); script.onerror = () => Reject(new Error(`Skriptladefehler für ${src}`)); document.head.append(script); }); }
Verwendung:
let Promise = LoadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); versprechen.then( script => warning(`${script.src} ist geladen!`), error => warning(`Fehler: ${error.message}`) ); Promise.then(script => warning('Another handler...'));
Wir können sofort einige Vorteile gegenüber dem rückrufbasierten Muster erkennen:
Versprechen | Rückrufe |
---|---|
Versprechen ermöglichen es uns, Dinge in der natürlichen Reihenfolge zu tun. Zuerst führen wir loadScript(script) aus und schreiben .then was mit dem Ergebnis geschehen soll. | Beim Aufruf von loadScript(script, callback) muss uns eine callback Funktion zur Verfügung stehen. Mit anderen Worten: Wir müssen wissen, was mit dem Ergebnis zu tun ist, bevor loadScript aufgerufen wird. |
Wir können .then so oft auf ein Promise zurückgreifen, wie wir wollen. Jedes Mal fügen wir der „Abonnementliste“ einen neuen „Fan“, eine neue Abonnementfunktion, hinzu. Mehr dazu im nächsten Kapitel: Verkettung von Versprechen. | Es kann nur ein Rückruf erfolgen. |
Versprechen geben uns also einen besseren Codefluss und mehr Flexibilität. Aber es gibt noch mehr. Das werden wir in den nächsten Kapiteln sehen.
Was ist die Ausgabe des folgenden Codes?
let versprochen = neues Versprechen(Funktion(auflösen, ablehnen) { auflösen(1); setTimeout(() => discover(2), 1000); }); versprechen.then(alert);
Die Ausgabe ist: 1
.
Der zweite Aufruf von resolve
wird ignoriert, da nur der erste Aufruf von reject/resolve
berücksichtigt wird. Weitere Anrufe werden ignoriert.
Die integrierte Funktion setTimeout
verwendet Rückrufe. Erstellen Sie eine versprechungsbasierte Alternative.
Die Funktion delay(ms)
sollte ein Versprechen zurückgeben. Dieses Versprechen sollte nach ms
Millisekunden aufgelöst werden, sodass wir .then
wie folgt hinzufügen können:
Funktionsverzögerung (ms) { // Dein Code } Verzögerung(3000).then(() => alarm('läuft nach 3 Sekunden'));
Funktionsverzögerung (ms) { return new Promise(resolve => setTimeout(resolve, ms)); } Verzögerung(3000).then(() => alarm('läuft nach 3 Sekunden'));
Bitte beachten Sie, dass in dieser Aufgabe resolve
ohne Argumente aufgerufen wird. Wir geben keinen Wert von delay
zurück, stellen lediglich die Verzögerung sicher.
Schreiben Sie die Funktion showCircle
in der Lösung der Aufgabe Animierter Kreis mit Rückruf um, sodass sie ein Versprechen zurückgibt, anstatt einen Rückruf anzunehmen.
Die neue Verwendung:
showCircle(150, 150, 100).then(div => { div.classList.add('message-ball'); div.append("Hallo Welt!"); });
Nehmen Sie die Lösung der Aufgabe Animierter Kreis mit Rückruf als Basis.
Öffnen Sie die Lösung in einer Sandbox.