Diese beiden am besten bewerteten Antworten sind falsch. Schauen Sie sich die MDN-Beschreibung des Parallelitätsmodells und der Ereignisschleife an , und es sollte klar werden, was los ist (diese MDN-Ressource ist ein echtes Juwel). Und einfach zu verwenden setTimeout
kann unerwartete Probleme in Ihren Code einfügen und dieses kleine Problem "lösen".
Was hier tatsächlich vor sich geht, ist nicht, dass "der Browser möglicherweise noch nicht ganz bereit ist, weil Parallelität" oder etwas, das auf "jede Zeile ist ein Ereignis, das am Ende der Warteschlange hinzugefügt wird" basiert.
Die von DVK bereitgestellte jsfiddle zeigt zwar ein Problem, aber seine Erklärung dafür ist nicht korrekt.
Was in seinem Code passiert, ist, dass er zuerst einen Ereignishandler an das click
Ereignis auf der #do
Schaltfläche anfügt.
Wenn Sie dann tatsächlich auf die Schaltfläche klicken, message
wird a erstellt, das auf die Ereignishandlerfunktion verweist, die der hinzugefügt wird message queue
. Wenn der event loop
diese Nachricht erreicht, wird frame
auf dem Stapel ein Funktionsaufruf für den Click-Event-Handler in der jsfiddle erstellt.
Und hier wird es interessant. Wir sind es so gewohnt, Javascript als asynchron zu betrachten, dass wir diese winzige Tatsache übersehen können: Jeder Frame muss vollständig ausgeführt werden, bevor der nächste Frame ausgeführt werden kann . Keine Parallelität, Leute.
Was bedeutet das? Dies bedeutet, dass bei jedem Aufruf einer Funktion aus der Nachrichtenwarteschlange die Warteschlange blockiert wird, bis der von ihr generierte Stapel geleert wurde. Oder allgemeiner gesagt, es wird blockiert, bis die Funktion zurückgekehrt ist. Und es blockiert alles , einschließlich DOM-Rendering-Operationen, Scrollen und so weiter. Wenn Sie eine Bestätigung wünschen, versuchen Sie einfach, die Dauer des lang laufenden Vorgangs in der Geige zu verlängern (z. B. die äußere Schleife noch 10 Mal ausführen), und Sie werden feststellen, dass Sie während der Ausführung nicht durch die Seite scrollen können. Wenn es lange genug läuft, werden Sie von Ihrem Browser gefragt, ob Sie den Vorgang abbrechen möchten, da die Seite dadurch nicht mehr reagiert. Der Frame wird ausgeführt, und die Ereignisschleife und die Nachrichtenwarteschlange bleiben hängen, bis sie beendet sind.
Warum wird dieser Nebeneffekt des Textes nicht aktualisiert? Denn während Sie haben den Wert des Elements in der DOM geändert - man kann console.log()
seinen Wert unmittelbar nach dem Wechsel und sehen , dass es hat sich geändert (die zeigt , warum DVK Erklärung nicht korrekt ist) - der Browser für den Stapel zu verarmen wartet (die on
Handlerfunktion, die zurückgegeben werden soll) und damit die Nachricht, die beendet werden soll, damit sie schließlich die Nachricht ausführen kann, die von der Laufzeit als Reaktion auf unsere Mutationsoperation hinzugefügt wurde, und um diese Mutation in der Benutzeroberfläche wiederzugeben .
Dies liegt daran, dass wir tatsächlich darauf warten, dass der Code ausgeführt wird. Wir haben nicht gesagt, dass "jemand dies abruft und diese Funktion dann mit den Ergebnissen aufruft, danke, und jetzt bin ich fertig, also kehren Sie zurück, machen Sie jetzt was auch immer", wie wir es normalerweise mit unserem ereignisbasierten asynchronen Javascript tun. Wir geben eine Funktion Ereignishandler klicken, wir ein DOM - Element aktualisieren, rufen wir eine weitere Funktion, die andere Funktion arbeitet für eine lange Zeit und dann zurückkehrt, wir dann das gleiche DOM - Element aktualisieren, und dann kehren wir von der anfänglichen Funktion, effektiv Entleerung der Stapel. Und dann kann der Browser zur nächsten Nachricht in der Warteschlange gelangen, bei der es sich möglicherweise um eine Nachricht handelt, die von uns durch Auslösen eines internen Ereignisses vom Typ "On-DOM-Mutation" generiert wurde.
Die Benutzeroberfläche des Browsers kann die Benutzeroberfläche nicht aktualisieren (oder möchte dies nicht tun), bis der aktuell ausgeführte Frame abgeschlossen ist (die Funktion ist zurückgekehrt). Persönlich denke ich, dass dies eher beabsichtigt als einschränkend ist.
Warum funktioniert das setTimeout
Ding dann? Dies geschieht, weil der Aufruf der Funktion mit langer Laufzeit effektiv aus dem eigenen Frame entfernt wird und die Ausführung später im window
Kontext geplant wird, sodass er selbst sofort zurückkehren und der Nachrichtenwarteschlange die Verarbeitung anderer Nachrichten ermöglichen kann. Die Idee ist, dass die Nachricht "On Update" der Benutzeroberfläche, die von uns in Javascript beim Ändern des Texts im DOM ausgelöst wurde, jetzt vor der Nachricht steht, die für die lang laufende Funktion in die Warteschlange gestellt wurde, sodass die Aktualisierung der Benutzeroberfläche vor dem Blockieren erfolgt Für eine lange Zeit.
Beachten Sie, dass a) die Langzeitfunktion bei der Ausführung immer noch alles blockiert und b) Ihnen nicht garantiert werden kann, dass das UI-Update in der Nachrichtenwarteschlange tatsächlich vor Ihnen liegt. In meinem Chrome-Browser vom Juni 2018 0
"behebt" ein Wert von nicht das Problem, das die Geige demonstriert - 10. Ich bin tatsächlich ein bisschen erstickt, weil es mir logisch erscheint, dass die UI-Aktualisierungsnachricht davor in die Warteschlange gestellt werden sollte, da ihr Trigger ausgeführt wird, bevor die Langzeitfunktion "später" ausgeführt wird. Aber vielleicht gibt es einige Optimierungen in der V8-Engine, die stören könnten, oder vielleicht fehlt mir einfach mein Verständnis.
Okay, was ist das Problem bei der Verwendung setTimeout
und was ist eine bessere Lösung für diesen speziellen Fall?
Zunächst einmal ist das Problem bei der Verwendung setTimeout
eines solchen Ereignishandlers, um ein anderes Problem zu beheben, anfällig für Probleme mit anderem Code. Hier ist ein reales Beispiel aus meiner Arbeit:
Ein Kollege versuchte in einem falsch informierten Verständnis der Ereignisschleife, Javascript zu "fädeln", indem er einen Code setTimeout 0
zum Rendern von Vorlagen für das Rendern verwendete. Er ist nicht mehr hier, um zu fragen, aber ich kann davon ausgehen, dass er möglicherweise Timer eingefügt hat, um die Rendergeschwindigkeit zu messen (was die sofortige Rückgabe von Funktionen bedeuten würde), und festgestellt hat, dass die Verwendung dieses Ansatzes zu unglaublich schnellen Antworten dieser Funktion führen würde.
Das erste Problem ist offensichtlich; Sie können kein Javascript einfädeln, daher gewinnen Sie hier nichts, während Sie die Verschleierung hinzufügen. Zweitens haben Sie jetzt das Rendern einer Vorlage effektiv vom Stapel möglicher Ereignis-Listener getrennt, die möglicherweise erwarten, dass genau diese Vorlage gerendert wurde, obwohl dies möglicherweise nicht der Fall war. Das tatsächliche Verhalten dieser Funktion war jetzt nicht deterministisch, ebenso wie - unwissentlich - jede Funktion, die sie ausführen würde oder von ihr abhängen würde. Sie können fundierte Vermutungen anstellen, aber Sie können das Verhalten nicht richtig codieren.
Das "Update" beim Schreiben eines neuen Ereignishandlers, der von seiner Logik abhing, sollte ebenfalls verwendet werden setTimeout 0
. Aber das ist keine Lösung, es ist schwer zu verstehen und es macht keinen Spaß, Fehler zu debuggen, die durch solchen Code verursacht werden. Manchmal gibt es nie ein Problem, manchmal schlägt es immer wieder fehl, und manchmal funktioniert es und bricht sporadisch ab, abhängig von der aktuellen Leistung der Plattform und dem, was gerade passiert. Aus diesem Grund würde ich persönlich davon abraten, diesen Hack (es) zu verwenden ist ein Hack, und wir sollten alle wissen, dass dies der Fall ist), es sei denn, Sie wissen wirklich, was Sie tun und welche Konsequenzen dies hat.
Aber was kann wir stattdessen tun? Wie aus dem MDN-Artikel hervorgeht, teilen Sie die Arbeit entweder in mehrere Nachrichten auf (wenn Sie können), damit andere Nachrichten, die sich in der Warteschlange befinden, mit Ihrer Arbeit verschachtelt und ausgeführt werden, während sie ausgeführt wird, oder verwenden Sie einen Web-Worker, der ausgeführt werden kann in Verbindung mit Ihrer Seite und geben Ergebnisse zurück, wenn die Berechnungen abgeschlossen sind.
Oh, und wenn Sie denken: "Nun, könnte ich nicht einfach einen Rückruf in die Langzeitfunktion einfügen, um sie asynchron zu machen?", Dann nein. Der Rückruf macht es nicht asynchron, es muss immer noch der lang laufende Code ausgeführt werden, bevor Ihr Rückruf explizit aufgerufen wird.