1. Warum ist JavaScript Single-Threaded?
Ein Hauptmerkmal der JavaScript -Sprache ist ein einzelnes Threading, was bedeutet, dass Sie nur eine Sache gleichzeitig tun können. Warum kann JavaScript nicht mehrere Threads haben? Dies wird die Effizienz verbessern.
Das einzelne Threading von JavaScript hängt mit seinem Zweck zusammen. Als Browser -Skriptsprache besteht der Hauptzweck von JavaScript darin, mit Benutzern zu interagieren und DOM zu betreiben. Dies stellt fest, dass es nur ein einzelnes Gewinde sein kann, andernfalls verursacht es sehr komplexe Synchronisationsprobleme. Angenommen, JavaScript hat zwei Threads gleichzeitig, ein Thread fügt einen Inhalt eines bestimmten DOM -Knotens hinzu, und der andere Thread löscht diesen Knoten. Welchen Thread sollte der Browser zu diesem Zeitpunkt aufnehmen?
Um Komplexität zu vermeiden, ist JavaScript daher ein einzelner Thread von seiner Geburt, der zum Kernmerkmal dieser Sprache geworden ist und sich in Zukunft nicht ändern wird.
Um die Rechenleistung von Multi-Core-CPUs zu nutzen, schlug HTML5 den Web-Worker-Standard vor, sodass JavaScript-Skripte mehrere Threads erstellen können. Die untergeordneten Threads werden jedoch vollständig vom Haupt-Thread gesteuert und können die DOM nicht bedienen. Daher ändert dieser neue Standard die Art von JavaScript -Einzel -Threading nicht.
2. Task Warteschlange
Ein einzelnes Threading bedeutet, dass alle Aufgaben in die Warteschlange gestellt werden müssen, und die vorherige Aufgabe wird ausgeführt, bevor die nächste Aufgabe ausgeführt wird. Wenn die vorherige Aufgabe lange dauert, muss die nächste Aufgabe warten.
Wenn die Warteschlange auf die große Menge an Computer zurückzuführen ist und die CPU zu beschäftigt ist, wäre sie in Ordnung, aber oft ist die CPU in Leerlauf, da das IO -Gerät (Eingangs- und Ausgabegerät) sehr langsam ist (z.
Der Designer der JavaScript -Sprache erkannte, dass die CPU zu diesem Zeitpunkt das IO -Gerät vollständig ignorieren, die wartenden Aufgaben suspendieren und zuerst die nächsten Aufgaben ausführen konnte. Warten Sie, bis das IO -Gerät das Ergebnis zurückgibt, dann drehen Sie sich um und setzen Sie die suspendierte Aufgabe fort.
Daher hat JavaScript zwei Ausführungsmethoden: Eine ist, dass die CPU nacheinander ausführt, die vorherige Aufgabe endet und dann die nächste Aufgabe ausgeführt wird, die als synchrone Ausführung bezeichnet wird. Das andere ist, dass die CPU Aufgaben mit einer langen Wartezeit überspringt und zuerst die nachfolgenden Aufgaben verarbeitet, die als asynchrone Ausführung bezeichnet werden. Programmierer wählen unabhängig, welche Art von Ausführungsmethode zu übernehmen ist.
Insbesondere ist der Betriebsmechanismus der asynchronen Ausführung wie folgt. (Gleiches gilt für die synchrone Ausführung, da sie als asynchrone Ausführung ohne asynchrone Aufgaben angesehen werden kann.)
(1) Alle Aufgaben werden im Haupt -Thread ausgeführt, um einen Ausführungskontextstapel zu bilden.
(2) Zusätzlich zum Haupt -Thread gibt es auch eine "Task -Warteschlange". Das System legt die asynchronen Aufgaben in die "Task -Warteschlange" und führt dann weiterhin nachfolgende Aufgaben aus.
(3) Sobald alle Aufgaben im "Ausführungsstapel" ausgeführt wurden, lesen das System die "Task -Warteschlange". Wenn die asynchrone Aufgabe zu diesem Zeitpunkt den Wartezustand beendet hat, wird der Ausführungsstapel aus der "Task -Warteschlange" und der Ausführung wieder aufgenommen.
(4) Der Hauptfaden wiederholt den dritten Schritt oben.
Die folgende Abbildung ist ein schematisches Diagramm der Hauptfaden- und Aufgabenwarteschlange.
Solange der Hauptfaden leer ist, lautet er die "Task -Warteschlange". Dies ist der laufende Mechanismus von JavaScript. Dieser Vorgang wird kontinuierlich wiederholt.
3. Ereignisse und Rückruffunktionen
"Task -Warteschlange" ist im Wesentlichen eine Warteschlange von Ereignissen (auch als Warteschlange von Nachrichten verstanden). Wenn ein IO -Gerät eine Aufgabe erledigt, fügt es der "Task -Warteschlange" ein Ereignis hinzu, was darauf hinweist, dass die relevanten asynchronen Aufgaben in den "Ausführungsstapel" eingeben können. Der Haupt -Thread liest die "Task -Warteschlange", was bedeutet, zu lesen, was Ereignisse darin sind.
Die Ereignisse in der "Task -Warteschlange" enthalten Ereignisse zusätzlich zu Ereignissen von IO -Geräten, aber auch Ereignisse, die von Benutzern generiert werden (z. B. Mausklicks, Seiten -Scrollen usw.). Solange die Rückruffunktion angegeben ist, geben diese Ereignisse die "Task -Warteschlange" ein, wenn sie auftreten, und warten, bis der Haupt -Thread gelesen wird.
Der sogenannte "Rückruf" ist der Code, der am Haupt-Thread aufgehängt wird. Asynchrone Aufgaben müssen eine Rückruffunktion angeben. Wenn die asynchrone Aufgabe von der "Task -Warteschlange" zum Ausführungsstapel zurückgibt, wird die Rückruffunktion ausgeführt.
"Task Queue" ist eine erste Datenstruktur mit Ereignissen, die zuerst eingestuft werden und zum Haupt-Thread zurückkehren. Der Lesevorgang des Hauptfadens ist im Grunde genommen automatisch. Solange der Ausführungsstapel gelöscht wird, kehrt das erste Ereignis in der "Task -Warteschlange" automatisch zum Haupt -Thread zurück. Aufgrund der später erwähnten "Timer" -Funktion muss der Haupt -Thread jedoch die Ausführungszeit überprüfen, und einige Ereignisse müssen zum Hauptthread zum angegebenen Zeitpunkt zurückkehren.
4. Ereignisschleife
Der Haupt -Thread liest Ereignisse aus der "Task -Warteschlange". Dieser Prozess wird kontinuierlich geschoben, sodass der gesamte laufende Mechanismus auch als Ereignisschleife bezeichnet wird.
Um die Ereignisschleife besser zu verstehen, sehen Sie sich das Bild unten an (zitiert aus Philip Roberts Rede "Hilfe, ich stecke in einer Event-Schleife fest").
In der obigen Abbildung erzeugt er, wenn der Hauptfaden ausgeführt wird, Haufen und Stapel. Der Code im Stack ruft verschiedene externe APIs auf, die der "Task -Warteschlange" verschiedene Ereignisse (klicken, laden, fertiggestellt) hinzufügen. Solange der Code im Stack ausgeführt wird, liest der Hauptthread die "Task -Warteschlange" und führt die Rückruffunktionen aus, die diesen Ereignissen wiederum entsprechen.
Führen Sie den Code im Stack aus, der immer ausgeführt wird, bevor Sie die "Task -Warteschlange" lesen. Bitte beachten Sie das folgende Beispiel.
Die Codekopie lautet wie folgt:
var req = new xmlhttprequest ();
req.open ('get', url);
req.onload = function () {};
req.onError = function () {};
req.send ();
Die REQ.Send -Methode im obigen Code ist eine AJAX -Operation, um Daten an den Server zu senden. Es handelt sich um eine asynchrone Aufgabe, was bedeutet, dass das System die "Task -Warteschlange" erst nach dem Ausführen des Codes im aktuellen Skript liest. Es entspricht also der folgenden Schreibmethode.
Die Codekopie lautet wie folgt:
var req = new xmlhttprequest ();
req.open ('get', url);
req.send ();
req.onload = function () {};
req.onError = function () {};
Das heißt, die Teile der angegebenen Rückruffunktion (Onload und OnError) sind vor oder nach der Send () -Methode nicht wichtig, da sie Teil des Ausführungsstapels sind, und das System wird sie immer vor dem Lesen der "Task -Warteschlange" ausführen.
5. Timer
Neben der Platzierung von asynchronen Aufgaben hat die "Task -Warteschlange" auch eine andere Funktion, die zeitgesteuerte Ereignisse platzieren soll, dh an, wie lange bestimmte Code ausgeführt wird. Dies wird als "Timer" -Funktion bezeichnet, der regelmäßig ausgeführt wird.
Die Timer -Funktion wird hauptsächlich durch zwei Funktionen ausgeführt: setTimeout () und setInterval (). Ihre internen Laufmechanismen sind genau gleich. Der Unterschied besteht darin, dass der von der ersteren angegebene Code gleichzeitig ausgeführt wird, während der letztere wiederholt ausgeführt wird. Im Folgenden wird hauptsächlich SetTimeout () erläutert.
setTimeout () akzeptiert zwei Parameter, die erste ist die Rückruffunktion, und die zweite ist die Anzahl der Millisekunden zur Verschiebung der Ausführung.
Die Codekopie lautet wie folgt:
Konsole.log (1);
setTimeout (function () {console.log (2);}, 1000);
console.log (3);
Das Ausführungsergebnis des obigen Code ist 1, 3, 2, da SetTimeout () die zweite Zeile bis nach 1000 Millisekunden verzögert.
Wenn der zweite Parameter von setTimeout () auf 0 gesetzt ist, bedeutet dies, dass die angegebene Rückruffunktion (0 Millisekunden -Intervall) unmittelbar nach der Ausführung des aktuellen Code ausgeführt wird (Ausführungsstapel löscht).
Die Codekopie lautet wie folgt:
setTimeout (function () {console.log (1);}, 0);
console.log (2);
Die Ausführungsergebnisse des obigen Codes sind immer 2 und 1, da das System die Rückruffunktion in der "Task -Warteschlange" erst nach Ausführung der zweiten Zeile ausführt.
Der HTML5 -Standard gibt an, dass der Mindestwert (kürzestes Intervall) des zweiten Parameters von SetTimeout () nicht weniger als 4 Millisekunden betragen darf. Wenn es niedriger als dieser Wert ist, erhöht sich automatisch. Vorher setzen ältere Browser das Mindestintervall auf 10 Millisekunden.
Darüber hinaus werden für diese DOM-Änderungen (insbesondere für die Teile, die die Seitenwiederholung beinhalten) normalerweise nicht sofort ausgeführt, sondern alle 16 Millisekunden. Zu diesem Zeitpunkt ist der Effekt der Verwendung von RequestAnimationFrame () besser als setTimeout ().
Es ist zu beachten, dass setTimeout () das Ereignis nur in die "Task -Warteschlange" einfügt. Sie müssen warten, bis der aktuelle Code (Ausführungsstack) ausgeführt wird, bevor der Hauptthread die angegebene Rückruffunktion ausführt. Wenn der aktuelle Code lange dauert, kann das Warten lange dauern. Es gibt keine Garantie dafür, dass die Rückruffunktion zum Zeitpunkt von SetTimeOut () ausgeführt wird.
6. Node.js Ereignisschleife
Node.js ist auch eine Ereignisschleife mit einem Thread, aber sein laufender Mechanismus unterscheidet sich von der der Browserumgebung.
Bitte beachten Sie das Diagramm unten (Autor @busyrich).
Nach der obigen Abbildung lautet der laufende Mechanismus von Node.js wie folgt.
(1) V8 Engine analysiert JavaScript -Skripte.
(2) Der analysierte Code ruft die Knoten -API auf.
(3) Die LiBUV -Bibliothek ist für die Ausführung der Knoten -API verantwortlich. Es weist verschiedenen Threads unterschiedliche Aufgaben zu, bildet eine Ereignisschleife und gibt die Ausführungsergebnisse der Aufgabe asynchron an die V8 -Engine zurück.
(4) Die V8 -Engine gibt das Ergebnis dem Benutzer zurück.
Zusätzlich zu den beiden Methoden setTimeout und setInterval bietet Node.js auch zwei weitere Methoden, die sich auf die "Task -Warteschlange" beziehen: process.nextTick und setImmediate. Sie können uns helfen, unser Verständnis von "Task -Warteschlangen" zu vertiefen.
Die Methode von Process.NextTick kann die Rückruffunktion am Ende des aktuellen "Ausführungsstapels" auslösen, bevor der Haupt -Thread das nächste Mal die "Task -Warteschlange" liest. Das heißt, die Aufgaben, die es spezifiziert, treten immer vor allen asynchronen Aufgaben auf. Die SetMediate -Methode löst die Rückruffunktion am Schwanz der aktuellen "Task -Warteschlange" aus, dh die von ihr angegebene Aufgabe wird immer ausgeführt, wenn der Haupt -Thread das nächste Mal die "Task -Warteschlange" liest, die SetTimeout sehr ähnlich ist (Fn, 0). Bitte beachten Sie das folgende Beispiel (über Stackoverflow).
Die Codekopie lautet wie folgt:
process.nextTick (Funktion a () {
Konsole.log (1);
process.nextTick (Funktion b () {console.log (2);});
});
setTimeout (Funktion Timeout () {
console.log ('Timeout gefeuert');
}, 0)
// 1
// 2
// Zeitlimit abgefeuert
In dem obigen Code wird im Hinblick auf die Methode des Prozesses. Dies bedeutet, dass, wenn es einen mehrfachen Prozess gibt.
Schauen wir uns nun SetImmediate an.
Die Codekopie lautet wie folgt:
setImmediate (Funktion a () {
Konsole.log (1);
setImmediate (Funktion b () {console.log (2);});
});
setTimeout (Funktion Timeout () {
console.log ('Timeout gefeuert');
}, 0)
// 1
// Zeitlimit abgefeuert
// 2
Im obigen Code gibt es zwei Setimmediates. Das erste SetMediat gibt an, dass die Rückruffunktion A am Schwanz der aktuellen "Task -Warteschlange" (die nächste "Ereignisschleife") ausgelöst wird. Anschließend gibt SetTimeOut auch an, dass das Timeout der Rückruffunktion am Schwanz der aktuellen "Task -Warteschlange" ausgelöst wird. Im Ausgabeergebnis wird das Timeout abgefeuert. 1. Wie für das 2 -Ranking hinter dem Zeitpunkt, das hinter dem Zeitpunkt abgefeuert wurde, ist dies daran, dass eine weitere wichtige Funktion von SetImmediate nur ausgelöst wird.
Wir haben einen wichtigen Unterschied dazu erhalten: multiple process.nextTick -Anweisungen werden immer gleichzeitig ausgeführt, während mehrere Setimmediates mehrmals ausgeführt werden müssen. In der Tat ist genau dies der Grund, warum Node.js Version 10.0 die SetMediate -Methode hinzufügt. Andernfalls ist der rekursive Aufruf zum Verfahren.
Die Codekopie lautet wie folgt:
process.nextTick (Funktion foo () {
process.nextTick (foo);
});
Wenn Sie nun einen rekursiven Prozess schreiben.
Da die durch procesal angegebene Rückruffunktion in dieser "Ereignisschleife" ausgelöst wird und SetMediate in der nächsten "Ereignisschleife" ausgelöst wird, ist es offensichtlich, dass das erstere immer früher als das letztere vorliegt und auch die Ausführung effizienter ist (da das "Task -Queue" nicht erforderlich ist, um das "Task -Queue" zu überprüfen ").