1. Warum ist JavaScript Single-Threaded?
Ein Hauptmerkmal der JavaScript-Sprache ist Single-Threading, was bedeutet, dass sie jeweils nur eine Sache ausführen kann. Warum kann JavaScript also nicht mehrere Threads haben? Dadurch kann die Effizienz verbessert werden.
Der einzelne Thread von JavaScript hängt mit seinem Zweck zusammen. Als Browser-Skriptsprache besteht der Hauptzweck von JavaScript darin, mit Benutzern zu interagieren und das DOM zu manipulieren. Dies legt fest, dass es nur Single-Threaded sein kann, da es sonst zu sehr komplexen Synchronisationsproblemen kommt. Angenommen, JavaScript verfügt über zwei Threads gleichzeitig. Ein Thread fügt Inhalt zu einem bestimmten DOM-Knoten hinzu und der andere Thread löscht den Knoten. Welchen Thread sollte der Browser in diesem Fall verwenden?
Um Komplexität zu vermeiden, ist JavaScript seit seiner Geburt Single-Threaded. Dies ist zu einem Kernmerkmal der Sprache geworden und wird sich auch in Zukunft nicht ändern.
Um die Rechenleistung von Multi-Core-CPUs zu nutzen, schlägt HTML5 den Web Worker-Standard vor, der es JavaScript-Skripten ermöglicht, mehrere Threads zu erstellen, die untergeordneten Threads werden jedoch vollständig vom Haupt-Thread gesteuert und dürfen den nicht bedienen DOM. Daher ändert dieser neue Standard nichts an der Single-Threaded-Natur von JavaScript.
2. Aufgabenwarteschlange
Einzelner Thread bedeutet, dass alle Aufgaben in die Warteschlange gestellt werden müssen und die nächste Aufgabe erst ausgeführt wird, wenn die vorherige Aufgabe abgeschlossen ist. Wenn die vorherige Aufgabe lange dauert, muss die nächste Aufgabe warten.
Wenn die Warteschlange aufgrund einer großen Berechnungsmenge und der CPU zu beschäftigt ist, vergessen Sie es, aber oft ist die CPU im Leerlauf, weil das E/A-Gerät (Eingabe- und Ausgabegerät) sehr langsam ist (z. B. Ajax-Operationen). Wenn Sie Daten aus dem Netzwerk lesen möchten, müssen Sie warten, bis die Ergebnisse vorliegen, bevor Sie fortfahren können.
Die Designer der JavaScript-Sprache haben erkannt, dass die CPU zu diesem Zeitpunkt das E/A-Gerät vollständig ignorieren, die wartenden Aufgaben anhalten und die späteren Aufgaben zuerst ausführen kann. Warten Sie, bis das E/A-Gerät das Ergebnis zurückgibt, gehen Sie dann zurück und fahren Sie mit der Ausführung der angehaltenen Aufgabe fort.
Daher verfügt JavaScript über zwei Ausführungsmethoden: Die eine besteht darin, dass die CPU sie nacheinander ausführt, die vorherige Aufgabe beendet und dann die nächste Aufgabe ausführt. Dies wird als synchrone Ausführung bezeichnet Bei langen Wartezeiten werden die nachfolgenden Aufgaben zuerst verarbeitet, was als asynchrone Ausführung bezeichnet wird. Es ist Sache des Programmierers, zu entscheiden, welche Ausführungsmethode er verwenden möchte.
Im Einzelnen ist der Betriebsmechanismus der asynchronen Ausführung wie folgt. (Dasselbe gilt für die synchrone Ausführung, da sie als asynchrone Ausführung ohne asynchrone Aufgabe betrachtet werden kann.)
(1) Alle Aufgaben werden im Hauptthread ausgeführt und bilden einen Ausführungskontextstapel.
(2) Zusätzlich zum Hauptthread gibt es auch eine „Aufgabenwarteschlange“. Das System stellt die asynchronen Aufgaben in die „Aufgabenwarteschlange“ und führt dann die Folgeaufgaben weiter aus.
(3) Sobald alle Aufgaben im „Ausführungsstapel“ ausgeführt wurden, liest das System die „Aufgabenwarteschlange“. Wenn die asynchrone Aufgabe zu diesem Zeitpunkt den Wartezustand beendet hat, gelangt sie aus der „Aufgabenwarteschlange“ in den Ausführungsstapel und nimmt die Ausführung wieder auf.
(4) Der Hauptthread wiederholt weiterhin den dritten Schritt oben.
Das Bild unten ist ein schematisches Diagramm des Hauptthreads und der Aufgabenwarteschlange.
Solange der Hauptthread leer ist, wird die „Aufgabenwarteschlange“ gelesen. Dies ist der laufende Mechanismus von JavaScript. Dieser Vorgang wiederholt sich ständig.
3. Ereignisse und Rückruffunktionen
„Aufgabenwarteschlange“ ist im Wesentlichen eine Ereigniswarteschlange (kann auch als Nachrichtenwarteschlange verstanden werden). Wenn das E/A-Gerät eine Aufgabe abschließt, wird ein Ereignis zur „Aufgabenwarteschlange“ hinzugefügt, um anzuzeigen, dass verwandte asynchrone Aufgaben eingegeben werden können. Ausführungsstapel". Der Hauptthread liest die „Aufgabenwarteschlange“, was bedeutet, dass die darin enthaltenen Ereignisse gelesen werden.
Ereignisse in der „Aufgabenwarteschlange“ umfassen neben IO-Geräteereignissen auch einige benutzergenerierte Ereignisse (z. B. Mausklicks, Seitenscrollen usw.). Solange die Rückruffunktion angegeben ist, werden diese Ereignisse bei ihrem Auftreten in die „Aufgabenwarteschlange“ eingegeben und warten auf das Lesen durch den Hauptthread.
Die sogenannte „Callback-Funktion“ ist der Code, der vom Hauptthread aufgehängt wird. Asynchrone Aufgaben müssen eine Rückruffunktion angeben. Wenn die asynchrone Aufgabe aus der „Aufgabenwarteschlange“ zum Ausführungsstapel zurückkehrt, wird die Rückruffunktion ausgeführt.
„Aufgabenwarteschlange“ ist eine First-In-First-Out-Datenstruktur. Die zuerst eingestuften Ereignisse werden zuerst an den Hauptthread zurückgegeben. Der Lesevorgang des Hauptthreads erfolgt grundsätzlich automatisch. Sobald der Ausführungsstapel gelöscht wird, kehrt das erste Ereignis in der „Aufgabenwarteschlange“ automatisch zum Hauptthread zurück. Aufgrund der später erwähnten „Timer“-Funktion muss der Hauptthread jedoch die Ausführungszeit überprüfen und bestimmte Ereignisse müssen zum angegebenen Zeitpunkt zum Hauptthread zurückkehren.
4. Ereignisschleife
Der Hauptthread liest Ereignisse aus der „Aufgabenwarteschlange“. Dieser Prozess ist zyklisch, daher wird der gesamte Betriebsmechanismus auch als Ereignisschleife bezeichnet.
Um Event Loop besser zu verstehen, schauen Sie sich bitte das Bild unten an (zitiert aus der Rede von Philip Roberts „Hilfe, ich stecke in einer Event-Schleife fest“).
Im Bild oben werden beim Ausführen des Hauptthreads ein Heap und ein Stack generiert. Der Code im Stack ruft verschiedene externe APIs auf und fügt der „Aufgabe“ verschiedene Ereignisse (Klick, Laden usw.) hinzu Warteschlange". fertig). Solange der Code im Stapel ausgeführt wird, liest der Hauptthread die „Aufgabenwarteschlange“ und führt die diesen Ereignissen entsprechenden Rückruffunktionen nacheinander aus.
Der Code im Ausführungsstapel wird immer ausgeführt, bevor die „Aufgabenwarteschlange“ gelesen wird. Schauen Sie sich das Beispiel unten an.
Zusätzlich zum Platzieren asynchroner Aufgaben hat die „Aufgabenwarteschlange“ auch eine Funktion: Sie kann zeitgesteuerte Ereignisse platzieren, also angeben, nach welcher Zeit bestimmter Code ausgeführt wird. Dies wird als „Timer“-Funktion bezeichnet, bei der es sich um Code handelt, der regelmäßig ausgeführt wird. Die Timer-Funktion wird hauptsächlich durch die beiden Funktionen setTimeout() und setInterval() vervollständigt. Ihre internen Betriebsmechanismen sind genau die gleichen. Der Unterschied besteht darin, dass der von erstere angegebene Code einmal ausgeführt wird, während letzterer wiederholt ausgeführt wird . Im Folgenden wird hauptsächlich setTimeout () erläutert.
setTimeout() akzeptiert zwei Parameter, der erste ist die Rückruffunktion und der zweite ist die Anzahl der Millisekunden, um die die Ausführung verzögert wird.
Code kopieren
Code kopieren
Das Ausführungsergebnis des obigen Codes ist immer 2,1, da das System erst nach Ausführung der zweiten Zeile die Rückruffunktion in der „Aufgabenwarteschlange“ ausführt.
Der HTML5-Standard schreibt vor, dass der Mindestwert (kürzestes Intervall) des zweiten Parameters von setTimeout() nicht weniger als 4 Millisekunden betragen darf. Wenn er niedriger als dieser Wert ist, wird er automatisch erhöht. Zuvor haben ältere Browser das Mindestintervall auf 10 Millisekunden festgelegt.
Außerdem werden diese DOM-Änderungen (insbesondere solche, die das erneute Rendern von Seiten beinhalten) normalerweise nicht sofort, sondern alle 16 Millisekunden ausgeführt. Derzeit ist der Effekt der Verwendung von requestAnimationFrame () besser als der von setTimeout ().
Es ist zu beachten, dass setTimeout() das Ereignis nur in die „Aufgabenwarteschlange“ einfügt. Der Hauptthread muss warten, bis die Ausführung des aktuellen Codes (Ausführungsstapels) abgeschlossen ist, bevor der Hauptthread die von ihm angegebene Rückruffunktion ausführt. Wenn der aktuelle Code lange dauert, kann es lange dauern, sodass nicht garantiert werden kann, dass die Rückruffunktion zu dem von setTimeout () angegebenen Zeitpunkt ausgeführt wird.
6. Ereignisschleife von Node.js
Node.js ist ebenfalls eine Single-Threaded-Ereignisschleife, ihr Betriebsmechanismus unterscheidet sich jedoch von der Browserumgebung.
Bitte sehen Sie sich das Diagramm unten an (von @BusyRich).
Gemäß dem Bild oben ist der Betriebsmechanismus von Node.js wie folgt.
(1) Die V8-Engine analysiert JavaScript-Skripte.
(2) Der analysierte Code ruft die Node-API auf.
(3) Die libuv-Bibliothek ist für die Ausführung der Node-API verantwortlich. Es weist verschiedenen Threads unterschiedliche Aufgaben zu, um eine Ereignisschleife (Ereignisschleife) zu bilden, und gibt die Ausführungsergebnisse der Aufgaben asynchron an die V8-Engine zurück.
(4) Die V8-Engine gibt die Ergebnisse an den Benutzer zurück.
Zusätzlich zu den beiden Methoden setTimeout und setInterval bietet Node.js auch zwei weitere Methoden im Zusammenhang mit der „Aufgabenwarteschlange“: process.nextTick und setImmediate. Sie können uns helfen, unser Verständnis der „Aufgabenwarteschlange“ zu vertiefen.
Die Methode „process.nextTick“ kann die Rückruffunktion am Ende des aktuellen „Ausführungsstapels“ auslösen, bevor der Hauptthread das nächste Mal die „Aufgabenwarteschlange“ liest. Das heißt, die angegebene Aufgabe wird immer vor allen asynchronen Aufgaben ausgeführt. Die setImmediate-Methode löst die Rückruffunktion am Ende der aktuellen „Aufgabenwarteschlange“ aus, dh die von ihr angegebene Aufgabe wird immer ausgeführt, wenn der Hauptthread das nächste Mal die „Aufgabenwarteschlange“ liest, was setTimeout( fn, 0) . Siehe das Beispiel unten (über StackOverflow).
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
Schauen Sie sich nun setImmediate an.
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// TIMEOUT FIRED
// 2
Im obigen Code gibt es zwei setImmediate. Das erste setImmediate gibt an, dass die Rückruffunktion A am Ende der aktuellen „Aufgabenwarteschlange“ (der nächsten „Ereignisschleife“) ausgelöst wird. Dann gibt setTimeout auch an, dass das Rückruffunktions-Timeout am Ende der aktuellen „Aufgabe“ ausgelöst wird queue“, daher steht TIMEOUT FIRED im Ausgabeergebnis hinter 1. Die Rangfolge 2 hinter TIMEOUT FIRED liegt an einer weiteren wichtigen Funktion von setImmediate: Eine „Ereignisschleife“ kann nur eine von setImmediate angegebene Rückruffunktion auslösen.
Dabei sehen wir einen wichtigen Unterschied: Mehrere „process.nextTick“-Anweisungen werden immer gleichzeitig ausgeführt, während mehrere „setImmediate“-Anweisungen mehrmals ausgeführt werden müssen. Tatsächlich ist dies der Grund, warum Node.js Version 10.0 die setImmediate-Methode hinzugefügt hat, da sonst der rekursive Aufruf von process.nextTick wie der folgende endlos ist und der Hauptthread die „Ereigniswarteschlange“ überhaupt nicht liest!