Kehren wir zu dem im Kapitel Einführung erwähnten Problem zurück: Rückrufe: Wir haben eine Folge asynchroner Aufgaben, die nacheinander ausgeführt werden müssen – zum Beispiel das Laden von Skripten. Wie können wir es gut codieren?
Versprechen bieten ein paar Rezepte dafür.
In diesem Kapitel behandeln wir die Verkettung von Versprechen.
Es sieht so aus:
neues Versprechen(Funktion(auflösen, ablehnen) { setTimeout(() => Auflösung(1), 1000); // (*) }).then(function(result) { // (**) Warnung(Ergebnis); // 1 Rückgabeergebnis * 2; }).then(function(result) { // (***) Warnung(Ergebnis); // 2 Rückgabeergebnis * 2; }).then(function(result) { Warnung(Ergebnis); // 4 Rückgabeergebnis * 2; });
Die Idee ist, dass das Ergebnis durch die Kette der .then
-Handler geleitet wird.
Hier ist der Ablauf:
Das anfängliche Versprechen wird in 1 Sekunde (*)
aufgelöst.
Dann wird der .then
Handler aufgerufen (**)
, der wiederum ein neues Versprechen erstellt (aufgelöst mit dem Wert 2
).
Der nächste then
(***)
ruft das Ergebnis des vorherigen ab, verarbeitet es (verdoppelt) und übergibt es an den nächsten Handler.
…und so weiter.
Während das Ergebnis entlang der Handlerkette weitergeleitet wird, können wir eine Folge von alert
sehen: 1
→ 2
→ 4
.
Das Ganze funktioniert, weil jeder Aufruf eines .then
ein neues Versprechen zurückgibt, sodass wir das nächste .then
darauf aufrufen können.
Wenn ein Handler einen Wert zurückgibt, wird er zum Ergebnis dieses Versprechens, sodass die nächste .then
Anweisung damit aufgerufen wird.
Ein klassischer Anfängerfehler: Technisch gesehen können wir auch viele .then
zu einem einzigen Versprechen hinzufügen. Das ist keine Verkettung.
Zum Beispiel:
let versprochen = neues Versprechen(Funktion(auflösen, ablehnen) { setTimeout(() => Auflösung(1), 1000); }); Versprechen.then(Funktion(Ergebnis) { Warnung(Ergebnis); // 1 Rückgabeergebnis * 2; }); Versprechen.then(Funktion(Ergebnis) { Warnung(Ergebnis); // 1 Rückgabeergebnis * 2; }); Versprechen.then(Funktion(Ergebnis) { Warnung(Ergebnis); // 1 Rückgabeergebnis * 2; });
Wir haben hier einfach mehrere Handler zu einem Versprechen hinzugefügt. Sie geben das Ergebnis nicht aneinander weiter; Stattdessen verarbeiten sie es unabhängig.
Hier ist das Bild (vergleichen Sie es mit der Verkettung oben):
Alle erhalten .then
mit demselben Versprechen das gleiche Ergebnis – das Ergebnis dieses Versprechens. Im obigen Code wird also bei allen alert
dasselbe angezeigt: 1
.
In der Praxis benötigen wir selten mehrere Handler für ein Versprechen. Viel häufiger kommt die Verkettung zum Einsatz.
Ein in .then(handler)
verwendeter Handler kann ein Versprechen erstellen und zurückgeben.
In diesem Fall warten weitere Handler, bis sich die Situation eingependelt hat, und erhalten dann ihr Ergebnis.
Zum Beispiel:
neues Versprechen(Funktion(auflösen, ablehnen) { setTimeout(() => Auflösung(1), 1000); }).then(function(result) { Warnung(Ergebnis); // 1 return new Promise((resolve, ablehne) => { // (*) setTimeout(() => Auflösung(Ergebnis * 2), 1000); }); }).then(function(result) { // (**) Warnung(Ergebnis); // 2 neues Versprechen zurückgeben((auflösen, ablehnen) => { setTimeout(() => Auflösung(Ergebnis * 2), 1000); }); }).then(function(result) { Warnung(Ergebnis); // 4 });
Hier zeigt das erste .then
1
und gibt new Promise(…)
in der Zeile (*)
zurück. Nach einer Sekunde wird es aufgelöst und das Ergebnis (das Argument von resolve
, hier ist es result * 2
) wird an den Handler des zweiten .then
weitergeleitet. Dieser Handler befindet sich in der Zeile (**)
, er zeigt 2
an und macht dasselbe.
Die Ausgabe ist also die gleiche wie im vorherigen Beispiel: 1 → 2 → 4, aber jetzt mit einer Verzögerung von 1 Sekunde zwischen den alert
.
Durch die Rückgabe von Versprechen können wir Ketten asynchroner Aktionen aufbauen.
Lassen Sie uns diese Funktion mit dem versprochenen loadScript
verwenden, das im vorherigen Kapitel definiert wurde, um Skripte einzeln und nacheinander zu laden:
LoadScript("https://javascript.info/article/promise-chaining/one.js") .then(function(script) { return loadScript("https://javascript.info/article/promise-chaining/two.js"); }) .then(function(script) { return loadScript("https://javascript.info/article/promise-chaining/ three.js"); }) .then(function(script) { // In Skripten deklarierte Funktionen verwenden // um zu zeigen, dass sie tatsächlich geladen haben eins(); zwei(); drei(); });
Dieser Code kann mit Pfeilfunktionen etwas kürzer gemacht werden:
LoadScript("https://javascript.info/article/promise-chaining/one.js") .then(script => loadScript("https://javascript.info/article/promise-chaining/two.js")) .then(script => loadScript("https://javascript.info/article/promise-chaining/ three.js")) .then(script => { // Skripte werden geladen, wir können dort deklarierte Funktionen verwenden eins(); zwei(); drei(); });
Hier gibt jeder loadScript
-Aufruf ein Versprechen zurück und das nächste .then
wird ausgeführt, wenn es aufgelöst wird. Anschließend wird das Laden des nächsten Skripts eingeleitet. So werden Skripte nacheinander geladen.
Wir können der Kette weitere asynchrone Aktionen hinzufügen. Bitte beachten Sie, dass der Code immer noch „flach“ ist – er wächst nach unten und nicht nach rechts. Es gibt keine Anzeichen für die „Pyramide des Untergangs“.
Technisch gesehen könnten wir .then
direkt zu jedem loadScript
hinzufügen, etwa so:
LoadScript("https://javascript.info/article/promise-chaining/one.js").then(script1 => { LoadScript("https://javascript.info/article/promise-chaining/two.js").then(script2 => { loadScript("https://javascript.info/article/promise-chaining/ three.js").then(script3 => { // Diese Funktion hat Zugriff auf die Variablen script1, script2 und script3 eins(); zwei(); drei(); }); }); });
Dieser Code macht dasselbe: Er lädt 3 Skripte nacheinander. Aber es „wächst nach rechts“. Wir haben also das gleiche Problem wie bei Rückrufen.
Leute, die anfangen, Versprechen zu verwenden, wissen manchmal nichts über Verkettungen, also schreiben sie es so. Im Allgemeinen wird eine Verkettung bevorzugt.
Manchmal ist es in Ordnung .then
direkt zu schreiben, da die verschachtelte Funktion Zugriff auf den äußeren Bereich hat. Im obigen Beispiel hat der am weitesten verschachtelte Rückruf Zugriff auf alle Variablen script1
, script2
, script3
. Aber das ist eher eine Ausnahme als eine Regel.
Thenables
Um genau zu sein, gibt ein Handler möglicherweise nicht gerade ein Versprechen zurück, sondern ein sogenanntes „thenable“-Objekt – ein beliebiges Objekt, das über eine Methode .then
verfügt. Es wird wie ein Versprechen behandelt.
Die Idee ist, dass Bibliotheken von Drittanbietern eigene „Versprechen-kompatible“ Objekte implementieren können. Sie können über einen erweiterten Methodensatz verfügen, aber auch mit nativen Versprechen kompatibel sein, da sie .then
implementieren.
Hier ist ein Beispiel für ein thenables Objekt:
Klasse Thenable { Konstruktor(Anzahl) { this.num = num; } then(lösen, ablehnen) { alarm(resolve); // function() { nativer Code } // nach 1 Sekunde mit this.num*2 auflösen setTimeout(() => discover(this.num * 2), 1000); // (**) } } neues Versprechen(auflösen => auflösen(1)) .then(result => { return new Thenable(result); // (*) }) .then(Alarm); // zeigt 2 nach 1000 ms
JavaScript überprüft das vom .then
Handler in Zeile (*)
zurückgegebene Objekt: Wenn es eine aufrufbare Methode mit dem Namen then
hat, ruft es diese Methode auf, die native Funktionen resolve
, reject
als Argumente bereitstellt (ähnlich einem Executor) und wartet, bis einer von ihnen heißt. Im obigen Beispiel wird resolve(2)
nach 1 Sekunde (**)
aufgerufen. Dann wird das Ergebnis weiter unten in der Kette weitergegeben.
Mit dieser Funktion können wir benutzerdefinierte Objekte in Promise-Ketten integrieren, ohne von Promise
erben zu müssen.
Bei der Frontend-Programmierung werden Versprechen häufig für Netzwerkanfragen verwendet. Sehen wir uns also ein erweitertes Beispiel dafür an.
Wir verwenden die Fetch-Methode, um die Informationen über den Benutzer vom Remote-Server zu laden. Es enthält viele optionale Parameter, die in separaten Kapiteln behandelt werden, aber die grundlegende Syntax ist recht einfach:
let Promise = fetch(url);
Dadurch wird eine Netzwerkanfrage an die url
gestellt und ein Versprechen zurückgegeben. Das Versprechen wird mit einem response
aufgelöst, wenn der Remote-Server mit Headern antwortet, aber bevor die vollständige Antwort heruntergeladen wird .
Um die vollständige Antwort zu lesen, sollten wir die Methode response.text()
aufrufen: Sie gibt ein Versprechen zurück, das aufgelöst wird, wenn der vollständige Text vom Remote-Server heruntergeladen wird, mit diesem Text als Ergebnis.
Der folgende Code stellt eine Anfrage an user.json
und lädt seinen Text vom Server:
fetch('https://javascript.info/article/promise-chaining/user.json') // .then unten wird ausgeführt, wenn der Remote-Server antwortet .then(Funktion(Antwort) { // Response.text() gibt ein neues Versprechen zurück, das mit dem vollständigen Antworttext aufgelöst wird // wenn es geladen wird return Response.text(); }) .then(Funktion(Text) { // ...und hier ist der Inhalt der Remote-Datei Warnung(Text); // {"name": "iliakan", "isAdmin": true} });
Das von fetch
zurückgegebene response
enthält auch die Methode response.json()
die die Remote-Daten liest und als JSON analysiert. In unserem Fall ist das sogar noch praktischer, also lasst uns darauf umsteigen.
Der Kürze halber verwenden wir auch Pfeilfunktionen:
// wie oben, aber Response.json() analysiert den Remote-Inhalt als JSON fetch('https://javascript.info/article/promise-chaining/user.json') .then(response => Response.json()) .then(user => alarm(user.name)); // iliakan, Benutzername erhalten
Jetzt machen wir etwas mit dem geladenen Benutzer.
Beispielsweise können wir eine weitere Anfrage an GitHub stellen, das Benutzerprofil laden und den Avatar anzeigen:
// Eine Anfrage für user.json stellen fetch('https://javascript.info/article/promise-chaining/user.json') // Laden Sie es als JSON .then(response => Response.json()) // Eine Anfrage an GitHub stellen .then(user => fetch(`https://api.github.com/users/${user.name}`)) // Die Antwort als JSON laden .then(response => Response.json()) // Das Avatar-Bild (githubUser.avatar_url) 3 Sekunden lang anzeigen (vielleicht animieren) .then(githubUser => { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => img.remove(), 3000); // (*) });
Der Code funktioniert; siehe Kommentare zu den Details. Darin liegt jedoch ein potenzielles Problem, ein typischer Fehler für diejenigen, die anfangen, Versprechen zu verwenden.
Schauen Sie sich die Zeile (*)
an: Wie können wir etwas tun, nachdem der Avatar nicht mehr angezeigt wird und entfernt wird? Beispielsweise möchten wir ein Formular zum Bearbeiten dieses Benutzers oder etwas anderem anzeigen. Im Moment gibt es keine Möglichkeit.
Um die Kette erweiterbar zu machen, müssen wir ein Versprechen zurückgeben, das aufgelöst wird, wenn der Avatar nicht mehr angezeigt wird.
So was:
fetch('https://javascript.info/article/promise-chaining/user.json') .then(response => Response.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(response => Response.json()) .then(githubUser => new Promise(function(resolve, ablehn) { // (*) let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); lösen(githubUser); // (**) }, 3000); })) // wird nach 3 Sekunden ausgelöst .then(githubUser => warning(`Anzeige von ${githubUser.name}` beendet));
Das heißt, der .then
Handler in Zeile (*)
gibt jetzt new Promise
zurück, das erst nach dem Aufruf von resolve(githubUser)
in setTimeout
(**)
abgewickelt wird. Das nächste .then
in der Kette wird darauf warten.
Als bewährte Vorgehensweise sollte eine asynchrone Aktion immer ein Versprechen zurückgeben. Dadurch ist es möglich, danach Aktionen zu planen; Auch wenn wir jetzt nicht vorhaben, die Kette zu verlängern, könnten wir sie später benötigen.
Schließlich können wir den Code in wiederverwendbare Funktionen aufteilen:
Funktion LoadJson(URL) { return fetch(URL) .then(response => Response.json()); } Funktion loadGithubUser(name) { return loadJson(`https://api.github.com/users/${name}`); } Funktion showAvatar(githubUser) { neues Versprechen zurückgeben(Funktion(auflösen, ablehnen) { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); lösen(githubUser); }, 3000); }); } // Benutze sie: loadJson('https://javascript.info/article/promise-chaining/user.json') .then(user => loadGithubUser(user.name)) .then(showAvatar) .then(githubUser => warning(`Anzeige von ${githubUser.name}` beendet)); // ...
Wenn ein .then
Handler (oder catch/finally
, spielt keine Rolle) ein Versprechen zurückgibt, wartet der Rest der Kette, bis es abgewickelt ist. Wenn dies der Fall ist, wird das Ergebnis (oder der Fehler) weitergegeben.
Hier ist ein vollständiges Bild:
Sind diese Codefragmente gleich? Mit anderen Worten: Verhalten sie sich unter allen Umständen und bei allen Handlerfunktionen gleich?
versprechen.then(f1).catch(f2);
Gegen:
versprechen.then(f1, f2);
Die kurze Antwort lautet: Nein, sie sind nicht gleich :
Der Unterschied besteht darin, dass ein Fehler, der in f1
auftritt, von .catch
hier behandelt wird:
versprechen .then(f1) .catch(f2);
…Aber nicht hier:
versprechen .then(f1, f2);
Das liegt daran, dass ein Fehler in der Kette weitergegeben wird und im zweiten Codeteil unterhalb von f1
keine Kette vorhanden ist.
Mit anderen Worten: .then
übergibt Ergebnisse/Fehler an das nächste .then/catch
. Im ersten Beispiel gibt es unten also einen catch
, im zweiten nicht, sodass der Fehler nicht behandelt wird.