Detaillierte Erläuterung des JavaScript-Betriebsmechanismus und eine kurze Diskussion über Event Loop

Detaillierte Erläuterung des JavaScript-Betriebsmechanismus und eine kurze Diskussion über Event Loop

1. Warum ist JavaScript Single-Threaded?

Ein Hauptmerkmal der Sprache JavaScript ist der Single-Thread-Betrieb, das heißt, es kann immer nur eine Sache gleichzeitig erledigt werden. Warum kann JavaScript nicht mehrere Threads haben? Dies kann die Effizienz verbessern.

JavaScript的單線程, was mit seinem Zweck zusammenhängt. Als Browser-Skriptsprache besteht der Hauptzweck von JavaScript darin, mit Benutzern zu interagieren und DOM zu manipulieren. Dies bestimmt, dass es nur ein Single-Thread sein kann, da es sonst zu sehr komplizierten Synchronisierungsproblemen kommt. Angenommen, JavaScript verfügt über zwei Threads gleichzeitig, ein Thread fügt einem DOM Knoten Inhalt hinzu und der andere Thread löscht den Knoten. Welchem ​​Thread soll der Browser folgen?

Um Komplexität zu vermeiden, ist JavaScript seit seiner Einführung Single-Threaded. Dies ist zu einem Kernmerkmal der Sprache geworden und wird sich in Zukunft nicht ändern.

Um die Rechenleistung von Mehrkern- CPU 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 Hauptthread gesteuert und dürfen DOM nicht bedienen. Daher ändert dieser neue Standard nichts an der Single-Thread-Natur von JavaScript.

2. Aufgabenwarteschlange

Single-Threaded 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 durch die große Menge an Berechnungen entsteht und CPU überlastet ist, ist das kein Problem, aber die meiste Zeit ist CPU im Leerlauf, weil die IO-Geräte (Eingabe- und Ausgabegeräte) sehr langsam sind (z. B. Ajax -Operationen, die Daten aus dem Netzwerk lesen) und sie muss warten, bis die Ergebnisse vorliegen, bevor sie mit der Ausführung fortfahren kann.

Die Entwickler der JavaScript-Sprache haben erkannt, dass der Hauptthread zu diesem Zeitpunkt das E/A-Gerät vollständig ignorieren, die wartenden Aufgaben anhalten und die später in die Warteschlange gestellten 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 können alle Aufgaben in zwei Typen unterteilt werden: synchrone Aufgaben ( synchronous ) und asynchronous Aufgaben (asynchron). Eine synchrone Aufgabe bezieht sich auf eine Aufgabe, die zur Ausführung im Hauptthread in die Warteschlange gestellt wird. Die nächste Aufgabe kann erst ausgeführt werden, nachdem die vorherige Aufgabe abgeschlossen ist. Eine asynchrone Aufgabe bezieht sich auf eine Aufgabe, die nicht in den Hauptthread, sondern in die „ task queue “ gelangt. Erst wenn die „Aufgabenwarteschlange“ den Hauptthread benachrichtigt, dass eine asynchrone Aufgabe ausgeführt werden kann, gelangt die Aufgabe zur Ausführung in den Hauptthread.

Im Einzelnen ist der Betriebsmechanismus der asynchronen Ausführung wie folgt. (Dasselbe gilt für die synchrone Ausführung, da sie ohne die asynchronen Aufgaben als asynchrone Ausführung betrachtet werden kann.)

(1) Alle synchronen Aufgaben werden auf dem Hauptthread ausgeführt und bilden einen execution context stack .
(2) Neben dem Hauptthread gibt es auch eine „ task queue “. Solange die asynchrone Aufgabe ein laufendes Ergebnis hat, wird ein Ereignis in die „Aufgabenwarteschlange“ gestellt.
(3) Sobald alle synchronen Aufgaben im „Ausführungsstapel“ ausgeführt wurden, liest das System die „Aufgabenwarteschlange“, um zu sehen, welche Ereignisse sich darin befinden. Die entsprechenden asynchronen Aufgaben beenden dann den Wartezustand, gelangen in den Ausführungsstapel und beginnen mit der Ausführung.
(4) Der Haupt-Thread wiederholt kontinuierlich den oben genannten Schritt 3.

Die folgende Abbildung ist ein schematisches Diagramm des Hauptthreads und der Task-Warteschlange.

Solange der Hauptthread leer ist, liest er die „Task-Warteschlange“, den Betriebsmechanismus von JavaScript . Dieser Vorgang wird sich wiederholen.

3. Ereignisse und Rückruffunktionen

Die „Aufgabenwarteschlange“ ist eine Ereigniswarteschlange (sie kann auch als Nachrichtenwarteschlange verstanden werden). Wenn ein E/A-Gerät eine Aufgabe abschließt, fügt es der „Aufgabenwarteschlange“ ein Ereignis hinzu, das angibt, dass die zugehörige asynchrone Aufgabe in den „Ausführungsstapel“ gelangen kann. Der Hauptthread liest die „Task-Warteschlange“, das heißt, er liest, welche Ereignisse sich darin befinden.

Die Ereignisse in der „Task-Warteschlange“ umfassen nicht nur Ereignisse von IO-Geräten, sondern auch einige von Benutzern generierte Ereignisse (wie Mausklicks, Seitenscrollen usw.). Solange die Rückruffunktion angegeben ist, gelangen diese Ereignisse bei ihrem Auftreten in die „Aufgabenwarteschlange“ und warten darauf, vom Hauptthread gelesen zu werden.

Die sogenannte „ callback -Funktion“ ist der Code, der vom Hauptthread angehalten wird. Asynchrone Aufgaben müssen eine Rückruffunktion angeben. Wenn der Hauptthread mit der Ausführung der asynchronen Aufgabe beginnt, führt er die entsprechende Rückruffunktion aus.

Die „Task-Warteschlange“ ist eine First-In-First-Out-Datenstruktur. Ereignisse am Anfang werden zuerst vom Hauptthread gelesen. Der Lesevorgang des Hauptthreads erfolgt grundsätzlich automatisch. Solange der Ausführungsstapel gelöscht ist, gelangt das erste Ereignis in der „Aufgabenwarteschlange“ automatisch in den Hauptthread. Aufgrund der später erwähnten Funktion „Timer“ muss der Hauptthread jedoch zunächst die Ausführungszeit überprüfen. Bestimmte Ereignisse können erst dann zum Hauptthread zurückkehren, wenn die angegebene Zeit erreicht ist.

4. Ereignisschleife

Der Hauptthread liest Ereignisse aus der „Task-Warteschlange“. Dieser Vorgang ist kontinuierlich, sodass der gesamte Betriebsmechanismus auch als Ereignisschleife bezeichnet wird.

Um Event Loop besser zu verstehen, schauen Sie sich bitte die folgende Abbildung an

In der obigen Abbildung werden beim Ausführen des Hauptthreads ein heap und ein stack generiert. Der Code im Stack ruft verschiedene externe APIs auf, die der „Task-Warteschlange“ verschiedene Ereignisse ( click , load , done ) hinzufügen. Solange der Code im Stapel ausgeführt wird, liest der Hauptthread die „Aufgabenwarteschlange“ und führt nacheinander die diesen Ereignissen entsprechenden Rückruffunktionen aus.

Der Code im Ausführungsstapel (synchrone Aufgaben) wird immer ausgeführt, bevor die „Aufgabenwarteschlange“ (asynchrone Aufgaben) gelesen wird. Bitte schauen Sie sich das folgende Beispiel an.

    var req = neue XMLHttpRequest();
    req.open('GET', URL);    
    req.onload = Funktion (){};    
    req.onerror = Funktion (){};    
    anfordern.senden();


Die Methode req.send im obigen Code ist eine Ajax -Operation, die Daten an den Server sendet. Es handelt sich um eine asynchrone Aufgabe, was bedeutet, dass das System die „Aufgabenwarteschlange“ erst liest, nachdem alle Codes im aktuellen Skript ausgeführt wurden. Daher ist es gleichbedeutend mit Folgendem.

    var req = neue XMLHttpRequest();
    req.open('GET', URL);
    anfordern.senden();
    req.onload = Funktion (){};    
    req.onerror = Funktion (){};   


Das heißt, es spielt keine Rolle, ob der Teil, der die Rückruffunktion angibt ( onload und onerror ), vor oder nach der send() Methode liegt, da sie Teil des Ausführungsstapels sind und das System sie immer ausführt, bevor die „Aufgabenwarteschlange“ gelesen wird.

5. Zeitgeber

Neben der Platzierung von Ereignissen für asynchrone Aufgaben kann die „Aufgabenwarteschlange“ auch zeitgesteuerte Ereignisse platzieren, d. h. angeben, wie viel Zeit für die Ausführung bestimmter Codes benötigt wird. Dies wird als „Timer“-Funktion bezeichnet. Dabei handelt es sich um Code, der zu einer festgelegten Zeit ausgeführt wird.

Die Timerfunktion wird hauptsächlich durch die beiden Funktionen setTimeout() und setInterval() vervollständigt. Ihre internen Betriebsmechanismen sind genau gleich. Der Unterschied besteht darin, dass der von der ersteren angegebene Code einmal ausgeführt wird, während der von der letzteren angegebene Code wiederholt ausgeführt wird. Im Folgenden wird hauptsächlich setTimeout()。

setTimeout() akzeptiert zwei Parameter, der erste ist die Rückruffunktion und der zweite ist die Anzahl der Millisekunden, um die die Ausführung verschoben werden soll.

konsole.log(1);
setTimeout(Funktion(){Konsole.log(2);},1000);
konsole.log(3);


Die Ausführungsergebnisse des obigen Codes sind 1, 3, 2, da setTimeout() die Ausführung der zweiten Zeile auf 1000 Millisekunden später verschiebt.

Wenn der zweite Parameter von setTimeout() auf 0 gesetzt ist, bedeutet dies, dass nach der Ausführung des aktuellen Codes (der Ausführungsstapel wird gelöscht) die angegebene Rückruffunktion sofort (mit einem Intervall von 0 Millisekunden) ausgeführt wird.

setTimeout(Funktion(){Konsole.log(1);}, 0);
konsole.log(2);


Das Ausführungsergebnis des obigen Codes ist immer 2, 1, da das System die Rückruffunktion in der „Task-Warteschlange“ erst nach Ausführung der zweiten Zeile ausführt.

Kurz gesagt besteht die Bedeutung von setTimeout(fn,0) darin, anzugeben, dass eine Aufgabe zum frühestmöglichen Leerlaufzeitpunkt des Hauptthreads ausgeführt werden soll, d. h. so früh wie möglich. Es fügt ein Ereignis am Ende der „Aufgabenwarteschlange“ hinzu, sodass es erst ausgeführt wird, wenn die Synchronisierungsaufgabe und die vorhandenen Ereignisse in der „Aufgabenwarteschlange“ verarbeitet wurden.

HTML5 Standard legt fest, dass der Mindestwert (kürzestes Intervall) des zweiten Parameters von setTimeout() nicht weniger als 4 Millisekunden betragen darf. Wenn er unter diesem Wert liegt, wird er automatisch erhöht. Zuvor war bei älteren Browserversionen das Mindestintervall auf 10 Millisekunden festgelegt. Darüber hinaus werden DOM-Änderungen (insbesondere solche, die eine erneute Seitendarstellung beinhalten) normalerweise nicht sofort, sondern alle 16 Millisekunden ausgeführt. Derzeit ist requestAnimationFrame() besser als setTimeout()。

Es ist zu beachten, dass setTimeout() das Ereignis nur in die „Aufgabenwarteschlange“ einfügt. Der Hauptthread führt die angegebene Rückruffunktion erst aus, wenn der aktuelle Code (Ausführungsstapel) ausgeführt wird. Wenn der aktuelle Code lange dauert, müssen Sie möglicherweise lange warten. Daher kann nicht garantiert werden, dass die Rückruffunktion zum von setTimeout() angegebenen Zeitpunkt ausgeführt wird.

6. Node.js-Ereignisschleife

Node.js verfügt ebenfalls über eine einfädige Event Loop , deren Funktionsmechanismus sich jedoch von dem der Browserumgebung unterscheidet.

Bitte sehen Sie sich das Diagramm unten an

Gemäß der obigen Abbildung ist der Betriebsmechanismus von Node.js wie folgt.

(1) Die V8-Engine analysiert JavaScript Skripte.

(2) Nach dem Parsen ruft der Code die Node-API auf.

(3) Die Bibliothek libuv ist für die Ausführung Node API verantwortlich. Es weist verschiedenen Threads unterschiedliche Aufgaben zu, um eine Event Loop zu bilden, und gibt die Ausführungsergebnisse der Aufgaben asynchron an die V8-Engine zurück.

(4) Anschließend gibt die V8-Engine das Ergebnis an den Benutzer zurück.

Zusätzlich zu setTimeout und setInterval bietet Node.js auch zwei weitere Methoden im Zusammenhang mit der „Task-Warteschlange“: process.nextTick und etImmediate . Sie können uns helfen, unser Verständnis von „Aufgabenwarteschlangen“ zu vertiefen.

process.nextTick kann die Rückruffunktion am Ende des aktuellen „Ausführungsstapels“ auslösen ---- vor der nächsten Event Loop (der Hauptthread liest die „Aufgabenwarteschlange“). Das heißt, die angegebene Aufgabe wird immer vor allen asynchronen Aufgaben ausgeführt. Die Methode setImmediate fügt am Ende der aktuellen „Aufgabenwarteschlange“ ein Ereignis hinzu, d. h. die von ihr angegebene Aufgabe wird immer bei der nächsten Event Loop ausgeführt, was setTimeout(fn, 0) sehr ähnlich ist. Siehe das Beispiel unten ( via StackOverflow ).

Prozess.nextTick(Funktion A() {
  konsole.log(1);
  Prozess.nextTick(Funktion B(){console.log(2);});
});

setzeTimeout(Funktion Timeout() {
  console.log('ZEITÜBERSCHREITUNG AUSGELÖST');
}, 0)
// 1
// 2
//TIMEOUT AUSGELÖST

Da im obigen Code die durch process.nextTick angegebene Rückruffunktion immer am Ende des aktuellen „Ausführungsstapels“ ausgelöst wird, wird nicht nur Funktion A vor dem durch setTimeout angegebenen Timeout der Rückruffunktion ausgeführt, sondern auch Funktion B vor timeout . Dies bedeutet, dass bei mehreren process.nextTick -Anweisungen (unabhängig davon, ob sie verschachtelt sind oder nicht) alle auf dem aktuellen „Ausführungsstapel“ ausgeführt werden.

Schauen wir uns nun setImmediate an

setzeSofort(Funktion A() {
  konsole.log(1);
  setImmediate(Funktion B(){Konsole.log(2);});
});

setzeTimeout(Funktion Timeout() {
  console.log('ZEITÜBERSCHREITUNG AUSGELÖST');
}, 0);

Im obigen Code fügen setImmediate und setTimeout(fn,0) jeweils eine Rückruffunktion A und timeout hinzu, die beide in der nächsten Event Loop ausgelöst werden. Also, welche Rückruffunktion wird zuerst ausgeführt? Die Antwort ist ungewiss. Das laufende Ergebnis kann 1--TIMEOUT FIRED--2 oder TIMEOUT FIRED--1--2。

Verwirrend ist, dass Node.js -Dokumentation steht, dass die durch setImmediate angegebene Rückruffunktion immer vor setTimeout platziert wird. In der Praxis passiert dies nur bei rekursiven Aufrufen.

setImmediate(Funktion (){
  setzeSofort(Funktion A() {
    konsole.log(1);
    setImmediate(Funktion B(){Konsole.log(2);});
  });

  setzeTimeout(Funktion Timeout() {
    console.log('ZEITÜBERSCHREITUNG AUSGELÖST');
  }, 0);
});
// 1
//TIMEOUT AUSGELÖST
// 2

Im obigen Code sind setImmediate und setTimeout in einem setImmediate gekapselt und sein Ausführungsergebnis ist immer 1--TIMEOUT FIRED--2. Zu diesem Zeitpunkt muss Funktion A vor dem Timeout ausgelöst werden. Die Tatsache, dass Funktion 2 nach TIMEOUT FIRED platziert wird (d. h. Funktion B wird nach timeout ausgelöst), liegt daran, dass setImmediate das Ereignis immer für die nächste Runde Event Loop registriert, sodass Funktion A und timeout in derselben Runde der Schleife ausgeführt werden, während Funktion B in der nächsten Runde der Schleife ausgeführt wird.

Daraus können wir einen wichtigen Unterschied zwischen process.nextTick und setImmediate erkennen: Mehrere process.nextTick -Anweisungen werden immer einmal im aktuellen „Ausführungsstapel“ ausgeführt, während mehrere setImmediate zur Ausführung mehrere Schleifen erfordern können. Genau aus diesem Grund wurde in Node.js Version 10.0 setImmediate hinzugefügt. Andernfalls wird der rekursive Aufruf process.nextTick wie folgt nie beendet und der Hauptthread liest die „Ereigniswarteschlange“ überhaupt nicht!

Prozess.nextTick(Funktion foo() {
  Prozess.nextTick(foo);
});


Wenn Sie process.nextTick,Node.js eine Warnung aus und fordert Sie auf, es setImmediate zu ändern.

Da außerdem die durch process.nextTick angegebene Rückruffunktion in der aktuellen „Ereignisschleife“ ausgelöst wird und setImmediate in der nächsten „Ereignisschleife“ ausgelöst wird, ist es offensichtlich, dass erstere immer früher auftritt als letztere und eine höhere Ausführungseffizienz aufweist (da die „Aufgabenwarteschlange“ nicht überprüft werden muss).

Damit ist dieser Artikel mit der detaillierten Erklärung des JavaScript Betriebsmechanismus und einer kurzen Erörterung von Event Loop abgeschlossen. Weitere Informationen zum JavaScript Betriebsmechanismus und Event Loop finden Sie in früheren Artikeln auf 123WORDPRESS.COM oder in den folgenden verwandten Artikeln. Ich hoffe, Sie werden 123WORDPRESS.COM auch in Zukunft unterstützen!

Das könnte Sie auch interessieren:
  • Detaillierte Erläuterung der Ereignisschleife (Event Loop) des JavaScript-Betriebsmechanismus
  • Javascript-Betriebsmechanismus „Event Loop“

<<:  Verwendung des Linux-Befehls xargs

>>:  Zusammenfassung einiger gängiger Protokolle in MySQL

Artikel empfehlen

Tutorial zur Konfiguration des Nginx/Httpd-Reverseproxys für Tomcat

Im vorherigen Blog erfuhren wir die Verwendung un...

jQuery-Plugin zur Implementierung des Dashboards

Das jQuery-Plug-In implementiert das Dashboard zu...

Einführung in Netzwerktreiber für Linux-Geräte

Kabelgebundenes Netzwerk: Ethernet Drahtloses Net...

Verwendung und Demonstration von ref in Vue

Ref-Definition: Wird verwendet, um Referenzinform...

Installationstutorial für MySQL 5.1 und 5.7 unter Linux

Das Betriebssystem für die folgenden Inhalte ist:...

Kann die wiederholbare Leseebene von MySQL Phantomlesevorgänge lösen?

Einführung Als ich mehr über die Datenbanktheorie...

JavaScript implementiert das mobile Puzzlespiel mit neun Rastern

In diesem Artikel wird der spezifische Code für J...

Implementierung eines Karussells mit nativem JavaScript

In diesem Artikel finden Sie den spezifischen Cod...

So fügen Sie Tastenkombinationen in Xshell hinzu

Als nützlicher Terminalemulator wird Xshell häufi...

Detailliertes Tutorial zur Installation von ElasticSearch 6.x im Docker

Ziehen Sie zuerst das Image (oder erstellen Sie e...

Mobiles Internet-Zeitalter: Responsive Webdesign ist zum allgemeinen Trend geworden

Wir befinden uns in einer Ära der rasanten Entwick...