Javascript verspricht Neugier

96

Wenn ich dieses Versprechen aufrufe, stimmt die Ausgabe nicht mit der Reihenfolge der Funktionsaufrufe überein. Das .thenkommt vor dem .catch, obwohl das Versprechen mit nachgefragt .thenwurde. Was ist der Grund dafür?

const verifier = (a, b) =>
  new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));

verifier(3, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

verifier(5, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

Ausgabe

node promises.js
response: true
error: false
Gustavo Alves
quelle
34
Sie sollten sich niemals auf Zeitabläufe zwischen unabhängigen Versprechungsketten verlassen.
Bergi

Antworten:

136

Dies ist eine coole Frage, um auf den Grund zu gehen.

Wenn Sie dies tun:

verifier(3,4).then(...)

Dadurch wird ein neues Versprechen zurückgegeben, für das ein weiterer Zyklus zur Ereignisschleife erforderlich ist, bevor dieses neu abgelehnte Versprechen den folgenden .catch()Handler ausführen kann . Dieser zusätzliche Zyklus ergibt die nächste Sequenz:

verifier(5,4).then(...)

Es besteht die Möglichkeit, den .then()Handler vor der vorherigen Zeile auszuführen , .catch()da er sich bereits in der Warteschlange befand, bevor der .catch()Handler der ersten Zeile in die Warteschlange gelangt und Elemente in der FIFO-Reihenfolge aus der Warteschlange ausgeführt werden.


Beachten Sie, dass das .then(f1, f2)Formular , wenn Sie es anstelle von verwenden .then().catch(), ausgeführt wird, wenn Sie es erwarten, da es kein zusätzliches Versprechen und somit kein zusätzliches Häkchen enthält:

const verifier = (a, b) =>
  new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));

verifier(3, 4)
  .then((response) => console.log("response (3,4): ", response),
        (error) => console.log("error (3,4): ", error)
  );

verifier(5, 4)
  .then((response) => console.log("response (5,4): ", response))
  .catch((error) => console.log("error (5,4): ", error));

Beachten Sie, dass ich auch alle Nachrichten beschriftet habe, damit Sie sehen können, von welchem verifier()Anruf sie kommen, was das Lesen der Ausgabe erheblich erleichtert.


ES6-Spezifikation zur Bestellung von Rückrufversprechen und ausführlichere Erläuterungen

Die ES6-Spezifikation sagt uns, dass Versprechen "Jobs" (wie es einen Rückruf von einem .then()oder aufruft .catch()) in FIFO-Reihenfolge ausgeführt werden, basierend darauf, wann sie in die Jobwarteschlange eingefügt werden. FIFO wird nicht speziell benannt, es wird jedoch angegeben, dass neue Jobs am Ende der Warteschlange eingefügt werden und Jobs ab dem Anfang der Warteschlange ausgeführt werden. Das implementiert die FIFO-Reihenfolge.

PerformPromiseThen (das den Rückruf von ausführt .then()) führt zu EnqueueJob. Auf diese Weise wird geplant, dass der Auflösungs- oder Ablehnungshandler tatsächlich ausgeführt wird. EnqueueJob gibt an, dass der ausstehende Job am Ende der Jobwarteschlange hinzugefügt wird. Dann zieht die NextJob- Operation das Element von der Vorderseite der Warteschlange. Dies stellt die FIFO-Reihenfolge bei der Bearbeitung von Jobs aus der Promise-Jobwarteschlange sicher.

Im Beispiel in der ursprünglichen Frage erhalten wir die Rückrufe für das verifier(3,4)Versprechen und das verifier(5,4)Versprechen, die in der Reihenfolge, in der sie ausgeführt wurden, in die Jobwarteschlange eingefügt wurden, da beide ursprünglichen Versprechen erfüllt sind. Wenn der Interpreter dann zur Ereignisschleife zurückkehrt, nimmt er zuerst den verifier(3,4)Job auf. Dieses Versprechen wird abgelehnt und es gibt keinen Rückruf dafür in der verifier(3,4).then(...). Es wird also das zurückgegebene Versprechen abgelehnt, das dazu führt verifier(3,4).then(...), dass der verifier(3,4).then(...).catch(...)Handler in die jobQueue eingefügt wird.

Dann kehrt es zur Ereignisschleife zurück und der nächste Job, den es aus der jobQueue abruft, ist der verifier(5, 4)Job. Das hat ein gelöstes Versprechen und einen entschlossenen Handler, also nennt es diesen Handler. Dadurch wird die response (5,4):Ausgabe angezeigt.

Dann kehrt es zur Ereignisschleife zurück und der nächste Job, den es aus der jobQueue abruft, ist der verifier(3,4).then(...).catch(...)Job, bei dem es ausgeführt wird, und dies verursacht dieerror (3,4) Ausgabe angezeigt.

Dies liegt daran, dass die .catch()in der 1. Kette eine Versprechungsstufe tiefer in ihrer Kette liegt als die .then()in der 2. Kette, die die von Ihnen gemeldete Bestellung verursacht. Dies liegt daran, dass Versprechen-Ketten nicht synchron über die Job-Warteschlange in FIFO-Reihenfolge von einer Ebene zur nächsten durchlaufen werden.


Allgemeine Empfehlung, sich auf diese Ebene der Planungsdetails zu verlassen

Zu Ihrer Information, im Allgemeinen versuche ich, Code zu schreiben, der nicht von diesem Grad an detailliertem Timing-Wissen abhängt. Es ist zwar neugierig und gelegentlich nützlich zu verstehen, aber es ist fragiler Code, da eine einfache, scheinbar harmlose Änderung des Codes zu einer Änderung des relativen Timings führen kann. Wenn das Timing zwischen zwei Ketten wie dieser kritisch ist, würde ich den Code lieber so schreiben, dass das Timing so erzwungen wird, wie ich es möchte, anstatt mich auf dieses detaillierte Verständnis zu verlassen.

jfriend00
quelle
Genauer gesagt ist dieses genaue Verhalten nirgendwo in der Spezifikation von Versprechungen dokumentiert, was dies zu einem Implementierungsdetail macht. Möglicherweise besteht ein unterschiedliches Verhalten zwischen Interpreten (z. B. Node.js vs Edge vs Firefox) oder zwischen Versionen von Interpreten (z. B. Node 12 vs Node 14). Die Spezifikation besagt lediglich, dass Versprechen asynchron verarbeitet werden, um Zalgo-Code zu vermeiden (was meiner Meinung nach übrigens falsch war, weil es von Leuten motiviert wurde, die Fragen wie diese stellten und vom Timing des potenziell asynchronen Codes abhängen wollten)
Slebetman
@slebetman - Ist nicht dokumentiert, dass Rückrufe von Versprechungen aus separaten Versprechungen als FIFO bezeichnet werden, je nachdem, wann sie in die Warteschlange eingefügt wurden und erst beim nächsten Tick ausgeführt werden können? Es scheint, dass hier nur die FIFO-Bestellung erforderlich ist, da .then()ein neues Versprechen zurückgegeben werden muss, das selbst bei einem zukünftigen Tick asynchron aufgelöst / abgelehnt werden muss, was zu dieser Bestellung führt. Kennen Sie eine Implementierung, bei der die FIFO-Reihenfolge konkurrierender Rückrufe nicht verwendet wird?
jfriend00
3
@slebetman Promises / A + gibt es nicht an. ES6 spezifiziert es. (ES11 hat jedoch das Verhalten von geändert await).
Bergi
Aus der ES6-Spezifikation in der Warteschlangenreihenfolge. PerformPromiseThenDies führt dazu EnqueueJob, dass der Auflösungs- oder Ablehnungshandler so aufgerufen wird, dass er aufgerufen wird. EnqueueJob gibt an, dass der ausstehende Job am Ende der Jobwarteschlange hinzugefügt wird. Dann zieht die NextJob- Operation das Element von der Vorderseite der Warteschlange. Dies stellt die FIFO-Reihenfolge in der Promise-Jobwarteschlange sicher.
jfriend00
@Bergi Was ist diese Änderung awaitin ES11? Ein Link reicht aus. Vielen Dank!!
Pedro A
49

Promise.resolve()
  .then(() => console.log('a1'))
  .then(() => console.log('a2'))
  .then(() => console.log('a3'))
Promise.resolve()
  .then(() => console.log('b1'))
  .then(() => console.log('b2'))
  .then(() => console.log('b3'))

Anstelle der Ausgabe a1, a2, a3, b1, b2, b3 sehen Sie aus demselben Grund a1, b1, a2, b2, a3, b3 - jedes Mal wird ein Versprechen zurückgegeben und es geht bis zum Ende der Ereignisschleife Warteschlange. Wir können also dieses "Versprechensrennen" sehen. Das gleiche gilt, wenn es einige verschachtelte Versprechen gibt.

Tarukami
quelle