2016-04-26 17 views
19

Ich möchte mir die Ausführungsreihenfolge des folgenden Snippets, das Javascript verspricht, erklären.Was ist die Reihenfolge der Ausführung in JavaScript verspricht

Promise.resolve('A') 
    .then(function(a){console.log(2, a); return 'B';}) 
    .then(function(a){ 
    Promise.resolve('C') 
     .then(function(a){console.log(7, a);}) 
     .then(function(a){console.log(8, a);}); 
    console.log(3, a); 
    return a;}) 
    .then(function(a){ 
    Promise.resolve('D') 
     .then(function(a){console.log(9, a);}) 
     .then(function(a){console.log(10, a);}); 
    console.log(4, a);}) 
    .then(function(a){ 
    console.log(5, a);}); 
console.log(1); 
setTimeout(function(){console.log(6)},0); 

Das Ergebnis ist:

1 
2 "A" 
3 "B" 
7 "C" 
4 "B" 
8 undefined 
9 "D" 
5 undefined 
10 undefined 
6 

Ich bin gespannt, um die Ausführungsreihenfolge 1 2 3 7 ... nicht die Werte 'A', 'B' ...

Mein Verständnis ist, dass, wenn ein Versprechen gelöst wird, die "then" -Funktion in die Ereigniswarteschlange des Browsers gestellt wird. Also meine Erwartung war 1 2 3 4 ...


@ jfriend00 Danke, vielen Dank für die detaillierten Erklärungen! Es ist wirklich eine enorme Arbeit!

+1

Versprechungen arbeiten mit 'return' Wert, sie sind nicht magisch :) Wenn Sie nicht von einem' then' zurückkehren, wird es nicht funktionieren. Wenn Sie 'return' hinzufügen, um Ihre Funktionen hinzuzufügen, funktioniert es wie erwartet. Es gibt 100 Duplikate, die nur darauf warten, dass bergi oder jfriend auf einen guten Punkt hinweisen. –

+0

* "Ich bin neugierig auf die Reihenfolge der Ausführung 1 2 3 7 ... nicht die Werte 'A', 'B' ..." * Entfernen Sie dann die überflüssigen Informationen und Code aus der Frage. –

+1

Wenn Sie nicht ausdrücklich vom Versprechen zurückgeben, wird implizit 'undefiniert' zurückgegeben. – Srle

Antwort

42

Kommentare

First off, verspricht innerhalb eines .then() Handler ausgeführt wird und nicht, dass diese Versprechungen der .then() Rückruf schafft eine völlig neue ungebunden Versprechen Sequenz zurückkehrt, die mit den Eltern Versprechungen nicht in irgendeiner Weise synchronisiert ist. In der Regel ist dies ein Fehler und in der Tat, einige Versprechen Motoren tatsächlich warnen, wenn Sie das tun, weil es fast nie das gewünschte Verhalten ist. Das einzige, was man jemals tun möchte, ist, wenn man eine Art von Feuer macht und Operationen vergisst, bei denen man sich nicht um Fehler kümmert und man sich nicht um die Synchronisierung mit dem Rest der Welt kümmert.

Also alle Ihre Promise.resolve() Versprechen innerhalb von Handler erstellen neue Promise-Ketten, die unabhängig von der Elternkette laufen. Sie haben kein bestimmtes Verhalten. Es ist ungefähr so, als würde man vier Ajax-Calls parallel starten. Sie wissen nicht, welcher zuerst fertig wird. Nun, da all dein Code innerhalb dieser Promise.resolve() Handler zufällig ist (da dies kein realer Weltcode ist), dann könntest du ein konsistentes Verhalten bekommen, aber das ist nicht der Designpunkt von Versprechen, also würde ich nicht viel Zeit damit verbringen versuchen herauszufinden, welche Promise-Kette, die nur synchronen Code ausführt, zuerst fertig wird. In der realen Welt spielt es keine Rolle, denn wenn es auf die Ordnung ankommt, dann werden Sie die Dinge nicht dem Zufall überlassen.

Zusammenfassung

  1. All .then() Handler werden asynchron nach dem aktuellen Ausführungs-Thread beendet (wie die Versprechen/A + spec sagt, wenn der JS Motor kehrt zurück zu "Plattform-Code") genannt. Dies gilt auch für Versprechen, die synchron gelöst werden, wie Promise.resolve().then(...).Dies wird für die Programmierungskonsistenz getan, so dass ein -Handler konsistent asynchron aufgerufen wird, unabhängig davon, ob das Versprechen sofort oder später gelöst wird. Dies verhindert einige Timing-Fehler und erleichtert es dem aufrufenden Code, konsistente asynchrone Ausführung zu sehen.

  2. Es gibt keine Spezifikation, die die relative Reihenfolge von setTimeout() vs. geplante Handler bestimmt, wenn beide in die Warteschlange eingereiht und bereit zum Ausführen sind. In Ihrer Implementierung wird ein ausstehender -Handler immer vor einem ausstehenden setTimeout() ausgeführt, aber die Spezifikationen der Spezifikation von Promises/A + sagen, dass dies nicht bestimmt ist. Es besagt, dass .then() Handler eine ganze Reihe von Wegen eingeplant werden können, von denen einige vor anstehenden setTimeout() Aufrufen ausgeführt werden und einige davon möglicherweise nach ausstehenden setTimeout() Aufrufen ausgeführt werden. Zum Beispiel ermöglicht die Promises/A + -Spezifikation, dass Handler entweder mit setImmediate() geplant werden, die vor anstehenden setTimeout() Aufrufen ausgeführt werden, oder mit setTimeout(), die nach ausstehenden setTimeout()-Aufrufen ausgeführt werden. Also sollte Ihr Code nicht von dieser Reihenfolge abhängen.

  3. Mehrere unabhängige Promise-Ketten haben keine vorhersehbare Reihenfolge der Ausführung und Sie können sich nicht auf eine bestimmte Reihenfolge verlassen. Es ist so, als würde man vier Ajax-Calls parallel abfeuern, von denen man nicht weiß, welcher zuerst fertig wird.

  4. Wenn die Reihenfolge der Ausführung wichtig ist, erstellen Sie kein Rennen, das von den Details der Minutenimplementierung abhängig ist. Verknüpfen Sie stattdessen Verheißungsketten, um eine bestimmte Ausführungsreihenfolge zu erzwingen.

  5. Im Allgemeinen möchten Sie keine unabhängigen Versprechensketten innerhalb eines -Handlers erstellen, die nicht vom Handler zurückgegeben werden. Dies ist normalerweise ein Fehler außer in seltenen Fällen von Feuer und vergessen ohne Fehlerbehandlung.

Zeile für Zeile analsysis

So, hier ist eine Analyse des Codes. Ich fügte hinzu, Zeilennummern und reinigte die Vertiefung, um es leichter zu verhandeln:

1  Promise.resolve('A').then(function (a) { 
2   console.log(2, a); 
3   return 'B'; 
4  }).then(function (a) { 
5   Promise.resolve('C').then(function (a) { 
6    console.log(7, a); 
7   }).then(function (a) { 
8    console.log(8, a); 
9   }); 
10  console.log(3, a); 
11  return a; 
12 }).then(function (a) { 
13  Promise.resolve('D').then(function (a) { 
14   console.log(9, a); 
15  }).then(function (a) { 
16   console.log(10, a); 
17  }); 
18  console.log(4, a); 
19 }).then(function (a) { 
20  console.log(5, a); 
21 }); 
22 
23 console.log(1); 
24  
25 setTimeout(function() { 
26  console.log(6) 
27 }, 0); 

Linie 1 ein Versprechen Kette beginnt und an einen .then() Handler zu. Da Promise.resolve() sofort aufgelöst wird, plant die Promise-Bibliothek die Ausführung des ersten -Handlers, nachdem dieser JavaScript-Thread beendet wurde. In kompatiblen Promises/A + -Versprechungsbibliotheken werden alle -Handler asynchron aufgerufen, nachdem der aktuelle Ausführungs-Thread beendet ist und wenn JS in die Ereignisschleife zurückkehrt. Dies bedeutet, dass jeder andere synchrone Code in diesem Thread wie Ihr console.log(1) als nächstes ausgeführt wird, was Sie sehen.

Alle anderen .then() Handler auf der obersten Ebene (Linien 4, 12, 19) -Kette nach dem ersten und wird erst nach der ersten wird wiederum ausgeführt. Sie sind im Wesentlichen an dieser Stelle eingereiht.

Da die setTimeout() auch in diesem ersten Thread der Ausführung ist, wird es ausgeführt und somit ist ein Timer eingeplant.

Das ist das Ende der synchronen Ausführung. Jetzt startet die JS-Engine Objekte, die in der Ereigniswarteschlange eingeplant sind.

Soweit ich weiß, gibt es keine Garantie, dass zuerst ein setTimeout(fn, 0) oder ein Handler, die beide geplant sind, direkt nach diesem Thread der Ausführung ausgeführt werden. .then() Handler werden als "Mikro-Aufgaben" betrachtet, so überrascht es mich nicht, dass sie zuerst vor der setTimeout() laufen. Wenn Sie jedoch eine bestimmte Bestellung benötigen, sollten Sie Code schreiben, der eine Bestellung garantiert, anstatt sich auf diese Implementierungsdetails zu verlassen.

Wie auch immer, die .then() Handler definiert auf Zeile 1 läuft als nächstes. Somit sehen Sie den Ausgang 2 "A" von diesem console.log(2, a).

Als nächstes, da der vorherige .then() Handler einen Normalwert zurückgeführt wird, dass Versprechen aufgelöst betrachtet, so dass die .then() Handler definiert auf Leitung 4 verläuft. Hier erstellen Sie eine weitere unabhängige Versprechenskette und führen ein Verhalten ein, bei dem es sich normalerweise um einen Fehler handelt.

Zeile 5, erstellt eine neue Promise-Kette. Es löst dieses erste Versprechen auf und plant dann zwei -Handler, die ausgeführt werden sollen, wenn der aktuelle Ausführungsthread beendet ist. In diesem aktuellen Thread der Ausführung ist die console.log(3, a) in Zeile 10, deshalb sehen Sie das als nächstes. Dann endet dieser Thread der Ausführung und es geht zurück zum Scheduler, um zu sehen, was als nächstes ausgeführt werden soll.

Wir haben nun mehrere .then() Handler in der Warteschlange, die darauf warten, als nächstes ausgeführt zu werden. Es gibt die, die wir gerade auf der Linie geplant 5 und es gibt die nächste in der höheren Ebene Kette auf der Leitung 12. Wenn Sie diese auf Linie 5 getan hatte:

return Promise.resolve.then(...) 

dann würden Sie diese Versprechen verknüpft haben zusammen und sie würden nacheinander koordiniert werden. Aber wenn Sie den Versprechenswert nicht zurückgeben, haben Sie eine ganz neue Versprechungskette begonnen, die nicht mit dem äußeren Versprechen auf höherer Ebene koordiniert ist. In Ihrem speziellen Fall entscheidet der Promise-Scheduler, den tiefer verschachtelten -Handler als nächstes auszuführen. Ich weiß nicht ehrlich, ob dies durch Spezifikation, durch Konvention oder nur durch ein Implementierungsdetail von einer Versprechensmaschine gegenüber der anderen geschieht. Ich würde sagen, wenn eine Bestellung für Sie wichtig ist, sollten Sie eine Bestellung erzwingen, indem Sie Versprechungen in einer bestimmten Reihenfolge verknüpfen, anstatt sich darauf zu verlassen, wer zuerst das Rennen gewinnt.

Wie auch immer, in Ihrem Fall, es ist ein Scheduling-Rennen und der Motor Sie entscheidet läuft die inneren .then() Handler auszuführen, die auf der Linie 5 weiter definiert sind und so sehen Sie die 7 "C" auf Linie 6 angegeben. Es gibt dann nichts zurück, so dass der aufgelöste Wert dieses Versprechens undefined wird.

Zurück im Scheduler, führt es die .then() Handler auf Zeile 12. Dies ist wiederum ein Rennen zwischen dem Handler und dem auf Linie 7, der ebenfalls auf den Lauf wartet. Ich weiß nicht, warum es hier einen anderen auswählt, außer zu sagen, dass es unbestimmt sein kann oder je Promotor variieren kann, weil die Reihenfolge nicht durch den Code spezifiziert ist. In jedem Fall beginnt der .then() Handler in Zeile 12 zu laufen. Das wiederum erzeugt eine neue unabhängige oder nicht synchronisierte Versprechens-Kettenlinie der vorherigen. Es plant wieder einen .then() Handler und dann erhalten Sie die 4 "B" aus dem synchronen Code in diesem Handler. Der gesamte synchrone Code ist in diesem Handler vorhanden, so dass er jetzt zum Scheduler für den nächsten Task zurückkehrt.

Zurück im Scheduler, entscheidet er die .then() Handler auf Linie 7 und Sie 8 undefined erhalten laufen. Das Versprechen dort ist undefined, weil der vorherige .then() Handler in dieser Kette nichts zurückgab, folglich war sein Rückgabewert undefined, folglich ist das der aufgelöste Wert der Versprechenkette an diesem Punkt.

An diesem Punkt wird der Ausgang so weit ist:

1 
2 "A" 
3 "B" 
7 "C" 
4 "B" 
8 undefined 

Auch hier werden alle synchronen Code ausgeführt wird, so dass es wieder an den Scheduler geht zurück und er beschließt, die .then() Handler auf Linie 13 definiert laufen . Das läuft und Sie erhalten die Ausgabe 9 "D" und dann geht es wieder zurück zum Scheduler.

In Übereinstimmung mit der zuvor geschachtelt Promise.resolve()-Kette, die der Zeitplan wählt die nächsten äußeren .then() Handler auf Leitung 19 definiert auszuführen. Es läuft und Sie erhalten die Ausgabe 5 undefined. Es ist wieder undefined, da der vorherige Handler in dieser Kette keinen Wert zurückgab, daher war der aufgelöste Wert des Versprechens undefined.

Da dieser Punkt, der Ausgang so weit ist:

1 
2 "A" 
3 "B" 
7 "C" 
4 "B" 
8 undefined 
9 "D" 
5 undefined 

An diesem Punkt gibt es nur einen .then() Handler, damit es ausgeführt werden soll geplant läuft das auf Linie 15 definiert und Sie das bekommen Ausgabe 10 undefined nächste.

Dann schließlich wird die setTimeout() laufen und die endgültige Ausgabe ist:

1 
2 "A" 
3 "B" 
7 "C" 
4 "B" 
8 undefined 
9 "D" 
5 undefined 
10 undefined 
6 

Wenn man genau vorherzusagen, um zu versuchen, waren dies würde in laufen, dann gäbe es zwei Hauptfragen sein.

  1. Wie anhängig .then() Handler priorisiert vs. setTimeout() Anrufe, die auch anstehen.

  2. Wie entscheidet sich die Promotor-Engine, mehrere Handler zu priorisieren, die alle darauf warten, ausgeführt zu werden. Pro Ergebnisse mit diesem Code ist es kein FIFO.

Für die erste Frage, ich weiß nicht, ob dies gemäß Spezifikation ist oder eine Implementierung Wahl gerade hier in dem Versprechen Motor/JS-Engine, aber die Umsetzung Sie erscheint berichtet alle anstehenden .then() Handler zu priorisieren, bevor alle setTimeout() Anrufe. Ihr Fall ist ein bisschen seltsam, weil Sie keine anderen asynchronen API-Aufrufe haben als die Angabe von Handlern. Wenn Sie eine asynchrone Operation ausgeführt hätten, die am Anfang dieser Versprechungskette wirklich in Echtzeit ausgeführt wurde, würde Ihre setTimeout() vor dem -Handler für die echte asynchrone Operation ausgeführt, nur weil die tatsächliche asynchrone Operation tatsächliche Ausführungszeit benötigt. Das ist ein bisschen ein konstruiertes Beispiel und ist nicht der übliche Design-Fall für echten Code.

Für die zweite Frage habe ich einige Diskussion gesehen, die diskutiert, wie Handler auf verschiedenen Ebenen der Verschachtelung priorisiert werden sollte. Ich weiß nicht, ob diese Diskussion jemals in einer Spezifikation gelöst wurde oder nicht. Ich bevorzuge es, so zu programmieren, dass mir diese Detailgenauigkeit egal ist. Wenn mir die Reihenfolge meiner asynchronen Operationen wichtig ist, verknüpfe ich meine Versprechensketten, um die Reihenfolge zu steuern, und diese Ebene der Implementierungsdetails beeinflusst mich in keiner Weise. Wenn mir der Auftrag egal ist, dann ist mir der Auftrag egal, so dass das Level der Implementierungsdetails mich nicht betrifft. Selbst wenn dies in einer bestimmten Spezifikation der Fall war, scheint es eine Art von Detail zu sein, dem in vielen verschiedenen Implementierungen (verschiedenen Browsern, verschiedenen Suchmaschinen) nicht vertraut werden sollte, es sei denn, Sie haben es überall getestet. Daher würde ich empfehlen, sich nicht auf eine bestimmte Reihenfolge der Ausführung zu verlassen, wenn Sie unsynchronisierte Versprechensketten haben.


Sie konnten den Auftrag 100% bestimmt, indem nur die Verknüpfung all Ihre Versprechen Ketten wie diese (innere Versprechen zurückkehrt, so dass sie in die Elternkette verbunden sind):

Promise.resolve('A').then(function (a) { 
    console.log(2, a); 
    return 'B'; 
}).then(function (a) { 
    var p = Promise.resolve('C').then(function (a) { 
     console.log(7, a); 
    }).then(function (a) { 
     console.log(8, a); 
    }); 
    console.log(3, a); 
    // return this promise to chain to the parent promise 
    return p; 
}).then(function (a) { 
    var p = Promise.resolve('D').then(function (a) { 
     console.log(9, a); 
    }).then(function (a) { 
     console.log(10, a); 
    }); 
    console.log(4, a); 
    // return this promise to chain to the parent promise 
    return p; 
}).then(function (a) { 
    console.log(5, a); 
}); 

console.log(1); 

setTimeout(function() { 
    console.log(6) 
}, 0); 

Dies ergibt die folgende Ausgabe in Chrome:

1 
2 "A" 
3 "B" 
7 "C" 
8 undefined 
4 undefined 
9 "D" 
10 undefined 
5 undefined 
6 

Und da das Versprechen haben alle miteinander verkettet sind, ist das Versprechen, um alle durch den Code definiert. Das einzige, was als Implementierungsdetail übrig bleibt, ist das Timing des setTimeout(), das wie in Ihrem Beispiel nach allen anstehenden Handlern zuletzt kommt.

Edit:

Bei der Untersuchung des Promises/A+ specification wir diese finden:

2.2.4 onFulfilled oder onRejected darf nicht aufgerufen werden, bis die Ausführungskontext Stapel enthält nur Plattform-Code. [3.1].

....

3.1 hier „Plattform Code“ Motor, Umwelt und Code-Implementierung versprechen. In der Praxis stellt diese Anforderung sicher, dass onFulfilled und onRejected asynchron ausgeführt werden, nach dem Ereignis Schleife drehen in der dann aufgerufen wird, und mit einem frischen Stapel. Dies kann entweder mit einem "Makro-Task" -Mechanismus wie setTimeout oder setImmediate oder mit einem "Mikro-Task" -Mechanismus wie MutationObserver oder process.nextTick implementiert werden. Da die Promise-Implementierung als Plattformcode betrachtet wird, kann sie selbst eine Task-Scheduling-Warteschlange oder "Trampolin" enthalten, in der die Handler aufgerufen werden.

Dies besagt, dass .then() Handler asynchron nach dem Aufruf-Stack wieder Plattform Code ausführen müssen, aber es ganz auf die Umsetzung läßt, wie genau das zu tun, ob es ist mit einer Makro-Aufgabe wie setTimeout() oder Mikro-Aufgabe erledigt wie process.nextTick(). Gemäß dieser Spezifikation ist es nicht bestimmt und sollte nicht verlässlich sein.

Ich finde keine Informationen über Makro-Aufgaben, Mikro-Aufgaben oder das Timing von Versprechen Handler in Bezug auf setTimeout() in der ES6-Spezifikation.Dies ist vielleicht nicht überraschend, da setTimeout() selbst nicht Teil der ES6-Spezifikation ist (es ist eine Host-Umgebungsfunktion, keine Sprachfunktion).

Ich habe keine Spezifikationen gefunden, um dies zu unterstützen, aber die Antworten auf diese Frage Difference between microtask and macrotask within an event loop context erklären, wie die Dinge in Browsern mit Makro-Aufgaben und Mikro-Aufgaben arbeiten.

FYI, wenn Sie mehr Informationen über Mikro-Aufgaben und Makro-Aufgaben wünschen, hier ist ein interessanter Referenzartikel zum Thema: Tasks, microtasks, queues and schedules.

+0

Informationen über das Timing von ausstehenden '.then()' Handlern und 'setTimeout()' aus der [Promises/A + Spezifikation] (https://promisesaplus.com/) hinzugefügt. – jfriend00

+1

Warum der Downvote? Ich habe viel Arbeit investiert, um zu erklären, wie das alles funktioniert. Zumindest könntest du erklären, warum du denkst, dass dies einen Downvote verdient. Wenn etwas ungenau ist und Sie darauf hinweisen können, werde ich es gerne korrigieren. – jfriend00

+0

"Soweit ich weiß, gibt es keine Garantie, die zuerst kommt ...", kommt es ganz auf die Umsetzung der Versprechen an. 'setTimeout' hat tatsächlich eine minimale Timeout-Verzögerung (etwas wie '15ms' IIRC), aber Promises * kann' setImmediate' verwenden, das natürlich vor einem gleichzeitig gesetzten Timer zur JS-Ereignisschleife hinzugefügt wird. Promise-Implementierungen verwenden oft 'setTimeout (fn, 0)', die dann in der Reihenfolge aufgelöst werden, in der ihre minimalen Timer ablaufen, was in der Reihenfolge geschehen würde, in der sie aufgerufen werden. – zzzzBov

1

Die JavaScript-Engine des Browsers hat eine so genannte "Event-Schleife". Es gibt immer nur einen Thread mit JavaScript-Code, der gleichzeitig ausgeführt wird. Wenn auf eine Schaltfläche geklickt oder eine AJAX-Anforderung oder ein anderer asynchroner Vorgang abgeschlossen wird, wird ein neues Ereignis in die Ereignisschleife eingefügt. Der Browser führt diese Ereignisse nacheinander aus.

Was Sie hier sehen, ist, dass Sie Code ausführen, der asynchron ausgeführt wird. Wenn der asynchrone Code abgeschlossen ist, fügt er der Ereignisschleife ein entsprechendes Ereignis hinzu. Die Reihenfolge, in der die Ereignisse hinzugefügt werden, hängt davon ab, wie lange die einzelnen asynchronen Vorgänge dauern.

Das bedeutet, wenn Sie etwas wie AJAX verwenden, bei dem Sie keine Kontrolle über die Reihenfolge haben, in der die Anforderungen ausgeführt werden, können Ihre Versprechen jedes Mal in einer anderen Reihenfolge ausgeführt werden.

+1

In den meisten Browsern werden '.then' Callbacks in einer Mikrotask-Warteschlange ausgeführt, die am Ende des aktuellen Laufs geleert wird. Vervollständigung, bevor sich die Hauptereignisschleife dreht. – jib

Verwandte Themen