Asynchrones JavaScript zu schreiben, ohne das Promise-Objekt zu verwenden, ist wie Kuchenbacken mit geschlossenen Augen. Es kann gemacht werden, aber es wird unordentlich sein und Sie werden sich wahrscheinlich verbrennen.
Ich sage nicht, dass es notwendig ist, aber Sie verstehen, was ich meine. Es ist wirklich gut. Manchmal braucht es jedoch ein wenig Hilfe, um einige einzigartige Herausforderungen zu lösen, wie z. B. wenn Sie eine Reihe von Promises sequenziell nacheinander auflösen möchten. Ein solcher Trick ist beispielsweise praktisch, wenn Sie eine Art Stapelverarbeitung über AJAX durchführen. Sie möchten, dass der Server eine Reihe von Dingen verarbeitet, aber nicht alle auf einmal, also verteilen Sie die Verarbeitung über die Zeit.
Wenn wir Pakete ausschließen, die diese Aufgabe erleichtern (wie die async Bibliothek von Caolan McMahon), ist die am häufigsten vorgeschlagene Lösung für die sequenzielle Auflösung von Promises die Verwendung von Array.prototype.reduce(). Von diesem haben Sie vielleicht schon gehört. Nehmen Sie eine Sammlung von Dingen und reduzieren Sie sie auf einen einzigen Wert, wie diesen
let result = [1,2,5].reduce((accumulator, item) => {
return accumulator + item;
}, 0); // <-- Our initial value.
console.log(result); // 8
Wenn wir reduce() jedoch für unsere Zwecke verwenden, sieht die Einrichtung eher so aus
let userIDs = [1,2,3];
userIDs.reduce( (previousPromise, nextID) => {
return previousPromise.then(() => {
return methodThatReturnsAPromise(nextID);
});
}, Promise.resolve());
Oder, in einem moderneren Format
let userIDs = [1,2,3];
userIDs.reduce( async (previousPromise, nextID) => {
await previousPromise;
return methodThatReturnsAPromise(nextID);
}, Promise.resolve());
Das ist nett! Aber sehr lange habe ich diese Lösung einfach geschluckt und diesen Codeblock in meine Anwendung kopiert, weil er "funktionierte". Dieser Beitrag ist mein Versuch, zwei Dinge zu verstehen:
- Warum funktioniert dieser Ansatz überhaupt?
- Warum können wir keine anderen
Array-Methoden verwenden, um dasselbe zu tun?
Warum funktioniert dieser Ansatz überhaupt?
Denken Sie daran, dass der Hauptzweck von reduce() darin besteht, eine Reihe von Dingen zu "reduzieren" und dies tut, indem das Ergebnis im accumulator gespeichert wird, während die Schleife läuft. Aber dieser accumulator muss nicht numerisch sein. Die Schleife kann alles zurückgeben, was sie möchte (wie ein Promise), und diesen Wert bei jeder Iteration über die Callback-Funktion wiederverwenden. Beachten Sie, dass die Schleife unabhängig vom Wert des accumulator ihr Verhalten, einschließlich der Ausführungsgeschwindigkeit, niemals ändert. Sie läuft einfach so schnell durch die Sammlung, wie es der Thread zulässt.
Das ist wichtig zu verstehen, denn es widerspricht wahrscheinlich dem, was Sie denken, was während dieser Schleife passiert (zumindest bei mir war das so). Wenn wir es zur sequenziellen Auflösung von Promises verwenden, verlangsamt sich die reduce()-Schleife tatsächlich überhaupt nicht. Sie ist vollständig synchron und tut ihr normales Ding, so schnell sie kann, wie immer.
Schauen Sie sich den folgenden Ausschnitt an und bemerken Sie, wie der Fortschritt der Schleife durch die im Callback zurückgegebenen Promises überhaupt nicht behindert wird.
function methodThatReturnsAPromise(nextID) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Resolve! ${dayjs().format('hh:mm:ss')}`);
resolve();
}, 1000);
});
}
[1,2,3].reduce( (accumulatorPromise, nextID) => {
console.log(`Loop! ${dayjs().format('hh:mm:ss')}`);
return accumulatorPromise.then(() => {
return methodThatReturnsAPromise(nextID);
});
}, Promise.resolve());
In unserer Konsole
"Loop! 11:28:06"
"Loop! 11:28:06"
"Loop! 11:28:06"
"Resolve! 11:28:07"
"Resolve! 11:28:08"
"Resolve! 11:28:09"
Die Promises lösen sich wie erwartet in der richtigen Reihenfolge auf, aber die Schleife selbst ist schnell, stetig und synchron. Nachdem ich mir die MDN-Polyfill für reduce() angesehen habe, macht das Sinn. Es gibt nichts Asynchrones an einer while()-Schleife, die immer wieder die callback() auslöst, was im Hintergrund passiert.
while (k < len) {
if (k in o) {
value = callback(value, o[k], k, o);
}
k++;
}
Mit all dem im Hinterkopf geschieht die eigentliche Magie in diesem Teil hier
return previousPromise.then(() => {
return methodThatReturnsAPromise(nextID)
});
Jedes Mal, wenn unser Callback ausgelöst wird, geben wir ein Promise zurück, das sich zu einem weiteren Promise auflöst. Und während reduce() nicht auf eine Auflösung wartet, bietet der Vorteil, dass wir nach jedem Durchlauf etwas an denselben Callback zurückgeben können, eine Funktion, die nur reduce() eigen ist. Infolgedessen können wir eine Kette von Promises aufbauen, die sich in weitere Promises auflösen, wodurch alles schön und sequenziell wird.
new Promise( (resolve, reject) => {
// Promise #1
resolve();
}).then( (result) => {
// Promise #2
return result;
}).then( (result) => {
// Promise #3
return result;
}); // ... and so on!
Das alles sollte auch offenlegen, warum wir nicht einfach jedes Mal ein einzelnes, neues Promise zurückgeben können. Da die Schleife synchron läuft, wird jedes Promise sofort ausgelöst, anstatt auf die zuvor erstellten zu warten.
[1,2,3].reduce( (previousPromise, nextID) => {
console.log(`Loop! ${dayjs().format('hh:mm:ss')}`);
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Resolve! ${dayjs().format('hh:mm:ss')}`);
resolve(nextID);
}, 1000);
});
}, Promise.resolve());
In unserer Konsole
"Loop! 11:31:20"
"Loop! 11:31:20"
"Loop! 11:31:20"
"Resolve! 11:31:21"
"Resolve! 11:31:21"
"Resolve! 11:31:21"
Ist es möglich zu warten, bis alle Verarbeitungen abgeschlossen sind, bevor etwas anderes getan wird? Ja. Die synchrone Natur von reduce() bedeutet nicht, dass Sie keine Party veranstalten können, nachdem jeder Artikel vollständig verarbeitet wurde. Schauen Sie
function methodThatReturnsAPromise(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Processing ${id}`);
resolve(id);
}, 1000);
});
}
let result = [1,2,3].reduce( (accumulatorPromise, nextID) => {
return accumulatorPromise.then(() => {
return methodThatReturnsAPromise(nextID);
});
}, Promise.resolve());
result.then(e => {
console.log("Resolution is complete! Let's party.")
});
Da wir in unserem Callback nur ein verkettetes Promise zurückgeben, ist das alles, was wir am Ende der Schleife erhalten: ein Promise. Danach können wir es nach Belieben behandeln, auch lange nachdem reduce() seinen Lauf beendet hat.
Warum funktionieren keine anderen Array-Methoden?
Denken Sie daran, dass wir im Hintergrund von reduce() nicht darauf warten, dass unser Callback abgeschlossen ist, bevor wir zum nächsten Element übergehen. Es ist völlig synchron. Das Gleiche gilt für all diese anderen Methoden
Array.prototype.map()Array.prototype.forEach()Array.prototype.filter()Array.prototype.some()Array.prototype.every()
Aber reduce() ist besonders.
Wir haben festgestellt, dass reduce() für uns funktioniert, weil wir etwas direkt an unseren gleichen Callback zurückgeben können (nämlich ein Promise), auf dem wir dann aufbauen können, indem wir es in ein weiteres Promise auflösen lassen. Bei all diesen anderen Methoden können wir jedoch kein Argument an unseren Callback übergeben, das von unserem Callback zurückgegeben wurde. Stattdessen sind alle diese Callback-Argumente vordefiniert, was es uns unmöglich macht, sie für etwas wie sequenzielle Promise-Auflösung zu nutzen.
[1,2,3].map((item, [index, array]) => [value]);
[1,2,3].filter((item, [index, array]) => [boolean]);
[1,2,3].some((item, [index, array]) => [boolean]);
[1,2,3].every((item, [index, array]) => [boolean]);
Ich hoffe, das hilft!
Ich hoffe, das wirft zumindest Licht darauf, warum reduce() sich auf diese Weise für die Behandlung von Promises eignet, und gibt Ihnen vielleicht ein besseres Verständnis dafür, wie gängige Array-Methoden im Hintergrund funktionieren. Habe ich etwas verpasst? Habe ich etwas falsch gemacht? Lassen Sie es mich wissen!
Das könnte irreführend sein. Sequenziell bedeutet, dass Sie
Loop...dannResolveerhalten, bevor Sie mit dem nächsten Promise fortfahren. Aber das macht keinen Unterschied zu einem normalenPromise.all(...).Hallo, Rong —
Ich verstehe die Verwirrung, aber es gibt tatsächlich einen bemerkenswerten Unterschied zwischen dem, worum es in diesem Beitrag geht, und dem, was
Promise.all()tut.Promise.all()ist dafür konzipiert, etwas zu tun, nachdem eine Sammlung von Promises alle aufgelöst wurden, unabhängig davon, in welcher Reihenfolge sie dies tun (sie könnten alle parallel aufgelöst werden undPromise.all()wäre zufrieden). Dieser Beitrag beschäftigt sich mehr damit, dass diese Promises alle der Reihe nach, einzeln und niemals parallel aufgelöst werden. Soweit ich weiß, gibt es keine Möglichkeit, dies mit etwas wiePromise.all()zu tun. Ich hoffe, das ergibt Sinn und dass ich Ihren Kommentar richtig verstanden habe.Ich habe noch nie eine Situation gefunden, in der reduce() sauberer war als eine einfache Schleife. In Ihrem Beispiel
let promise = Promise.resolve();
for (const nextID of userIDs) {
promise = promise.then(() => methodThatReturnsAPromise(nextID));
}
ist viel einfacher zu lesen und zu verstehen. Noch schlimmer ist es, wenn Leute reduce() verwendeten, um ein Objekt zu verändern, und die Reducer-Funktion einfach ihr erstes Argument zurückgibt. Das beeinträchtigt die Lesbarkeit erheblich, um vielleicht eine temporäre Variable zu vermeiden.
Das sind einige großartige Punkte. Der
for...in-Ansatz ist definitiv besser lesbar; ich bin neugierig, ob der Reiz vonreduce()über das Einsparen einer Variablenvereinbarung hinausgeht.Darüber hinaus wäre eine Untersuchung, warum DIESER Ansatz für die sequenzielle Auflösung funktioniert, super interessant. Ist eine
for...in-Schleife nicht immer synchron?Oder wenn Sie async/await verwenden können, ist es einfach
Perfekt! Auf den Punkt gebracht. Hat Spaß gemacht zu lesen.
Führen Sie ein Map innerhalb eines Promise.all aus. Wenn die in der Map ausgeführte Funktion async/await verwendet, wird sie vor der nächsten Iteration abgeschlossen (der Reihenfolge nach, nicht parallel). Der Bonus ist, dass Promise.all all Ihre Promise-Werte in einem Array auflöst. Ich poste das von meinem iPad, sonst würde ich ein Codebeispiel geben.
Warum nicht einfach rxjs verwenden?
Ich muss sagen, das ist eine eher unkonventionelle Methode zur Behandlung von Promises, die man nicht oft im Internet findet, und es ist immer schön, neue Wege zu erkunden. Danke für diesen Artikel, Alex!
Ich verstehe nicht ganz, warum es so viele "Hass"-Kommentare gibt, es ist ja nicht so, dass es eine universelle Methode gibt, etwas in der Programmierung zu handhaben -> das richtige Werkzeug für den richtigen Job, je nach Umständen.
Wenn ich mehrere Promises behandeln muss, benutze ich im Allgemeinen
Promise.all, wie einige Leute geschrieben haben. Ich hatte noch nie eine Situation, in der ich einen Ansatz wie diesen gebraucht hätte (obwohl es gut ist, ihn zu kennen).Heutzutage können Browser mit HTTP/2 problemlos mehrere gleichzeitige Anfragen bearbeiten, und wenn Sie viele davon haben, würden Ihre Benutzer definitiv lange auf sequentielle Anfragen warten, die eine nach der anderen aufgelöst werden.
Außerdem, wenn wir eine Situation hätten, in der wir Promises sequentiell ausführen müssen, wie z. B. eine Factory-Methode, die einen Server anpingt, um ein globales Konfigurationsobjekt zu erstellen, wäre dieser Code nicht ein wenig eng gekoppelt?
Können Sie mir ein Beispiel für eine reale Anwendung dafür geben?
Ich würde diesen Ansatz überhaupt nicht empfehlen. 1. Warum sollte ich einen synchronen Ansatz mit asynchronen Mitteln verwenden, besser als keine Promises zu verwenden. 2. Führt zu Verwirrung und kann versteckte Probleme einführen. Ich hatte eine Situation, in der jemand in der Hoffnung, langsame Codes zu beheben, genau dies mit reduce codiert hatte, indem er Promises in der richtigen Reihenfolge auflöste (und das Lustige war, dass der Promise-Callback eine normale Funktion mit einem setTimeout von 1 war). Das Stück Code war nicht nur schwer zu debuggen, sondern verbarg auch die tatsächliche Langsamkeit, die woanders lag.
Wenn es einen realen Fall gibt, in dem Sie dies tun müssen, dann überdenken Sie Ihren Code erneut, Sie machen möglicherweise etwas falsch.
Mein Rat ist, solche "Cheat"-Anti-Patterns generell zu vermeiden.
Mach dein Ding, Bro! Dieser Beitrag ist überhaupt nicht vorschreibend. Er ist nur daran interessiert zu erklären, warum der
reduce()-Ansatz funktioniert, auch wenn er so verbreitet ist. Je vertrauter ich mit solchen Schleifen geworden bin, desto unsicherer bin ich ehrlich gesagt, ob ich ihn selbst verwenden würde. Ich denke, das hängt stark vom Anwendungsfall ab.Sobald Sie eine Netzwerkanfrage stellen müssen, haben Sie jetzt asynchronen Code. Sie haben also keine Wahl. Wenn Sie Dinge sequenziell erledigen müssen (z. B. garantieren, dass Dinge nacheinander in eine Datenbank eingegeben werden), dann muss es sich synchron verhalten. Ich könnte weitere Beispiele geben, aber das Wichtigste hier ist, dass dies eine gültige Lösung für bestimmte Situationen ist.
@Mike Netzwerkanfragen können letztendlich synchron erfolgen (synchrone AJAX), aber das ist nicht der Anwendungsfall, da dies die Benutzeroberfläche des Benutzers einfriert.
Die zweite Aussage, die Sie als falschen Anwendungsfall bezeichnen, weil Sie niemals eine sequentielle Aktion im Frontend garantieren können (ich kann immer mit benutzerdefiniertem JS betrügen). Alle Validierungen sollten im Backend erfolgen, unabhängig davon, was das Frontend garantiert.
Wenn Sie mir weitere Beispiele geben, kann ich Ihnen zeigen, ob sie falsch oder richtig sind. Das Datenbankbeispiel ist ein schlechter Anwendungsfall.
Ok, ein weiteres Beispiel. Sie sind in Node. Sie haben eine Datenbank als Dienst (sagen wir Firebase). Sie interagieren mit der Datenbank über die offizielle Firebase Node-Bibliothek, die Promises zurückgibt. Sie müssen Operationen in sequenzieller Reihenfolge ausführen.
@Mike nochmals, das Datenbankbeispiel ist nicht korrekt, egal wer der Client und wer der Server ist. In Ihrem Beispiel: Sie senden einfach den Index Ihres Elements im Array und speichern das als Ordnungszahl, keine Notwendigkeit, sequenziell auszuführen.
Selbst wenn Sie sie sequenziell senden, was ist die Reihenfolge bei Firebase? Die ID, die eine zufällige Zeichenkette bei Firebase ist?
Was passiert, wenn ein Promise fehlschlägt?
Wenn Sie wirklich die Reihenfolge benötigen, senden Sie den Array-Index Ihres Elements und lösen Sie die Promises parallel auf.
Ich kann Ihnen beweisen, dass jeder Anwendungsfall dieser Methode auf wesentlich einfachere und bessere Weise erledigt werden kann.
Hat jemand bemerkt, dass das letzte Promise nicht aufgelöst wird? Wenn wir also 3 userIds haben, werden wir nur die ersten beiden davon verarbeiten.
Prost!
Ich stimme Alex zu, das ist nur ein interessanter Anwendungsfall, aber sehr, sehr selten für reale Anwendungsfälle. Tatsächlich kann ich keinen dafür als beste Alternative finden.
Ich mag deinen Beitrag! Daher möchte ich dich fragen, ob ich deinen Beitrag ins Chinesische übersetzen und auf meinem Blog veröffentlichen kann. Natürlich werde ich einen Link zu deinem Beitrag hinzufügen und am Anfang meines Blogs angeben, dass mein Beitrag von deinem übersetzt wurde.
Hallo! Klar, Sie können alles auf der Website verwenden: https://css-tricks.de/license/