JavaScript ist eine Single-Thread-Sprache und gleichzeitig auch nicht-blockierend, asynchron und nebenläufig. Dieser Artikel wird Ihnen erklären, wie das geschieht.
Laufzeit
JavaScript ist eine interpretierte Sprache, keine kompilierte. Das bedeutet, dass sie einen Interpreter benötigt, der die JS Code in einen Maschinencode. Es gibt verschiedene Arten von Interpretern (so genannte Engines). Die beliebtesten Browser-Engines sind V8 (Chrome), Quantum (Firefox) und WebKit (Safari). V8 wird übrigens auch in einer beliebten Nicht-Browser-Laufzeitumgebung verwendet, Node.js.
Jede Engine enthält einen Speicher-Heap, einen Call-Stack, eine Ereignisschleife, eine Callback-Warteschlange und eine WebAPI mit HTTP-Anfragen, Timern, Ereignissen usw., die alle auf ihre eigene Weise implementiert sind, um eine schnellere und sicherere Interpretation des JS-Codes zu gewährleisten.
Grundlegende JS-Laufzeitarchitektur. Autor: Alex Zlatkov
Einzelnes Thema
Eine Single-Thread-Sprache ist eine Sprache mit einem einzigen Aufrufstapel und einem einzigen Speicherheap. Das bedeutet, dass sie nur eine Sache zur gleichen Zeit ausführt.
A Stapel ist ein kontinuierlicher Speicherbereich, der für jede ausgeführte Funktion einen lokalen Kontext zuweist.
A Haufen ist ein viel größerer Bereich, in dem alles dynamisch zugewiesen wird.
A Aufrufstapel ist eine Datenstruktur, die im Wesentlichen aufzeichnet, wo wir uns im Programm befinden.
Stapel aufrufen
Schreiben wir einen einfachen Code und verfolgen wir, was auf dem Aufrufstapel passiert.
Wie Sie sehen können, werden die Funktionen dem Stapel hinzugefügt, ausgeführt und später gelöscht. Das ist der sogenannte LIFO-Weg - Last In, First Out. Jeder Eintrag im Aufrufstapel wird als Stapelrahmen.
Die Kenntnis des Aufrufstapels ist nützlich für das Lesen von Fehler-Stapelspuren. Im Allgemeinen steht der genaue Grund für den Fehler oben in der ersten Zeile, obwohl die Reihenfolge der Codeausführung von unten nach oben verläuft.
Manchmal können Sie mit einem beliebten Fehler umgehen, der durch Maximale Größe des Aufrufstapels überschritten. Mit Hilfe der Rekursion ist dies leicht zu erreichen:
function foo() {
foo()
}
foo()
und unser Browser oder Terminal friert ein. Jeder Browser, auch die verschiedenen Versionen, hat eine andere Grenze für die Größe des Aufrufstapels. In der überwiegenden Mehrheit der Fälle sind sie ausreichend und das Problem sollte anderswo gesucht werden.
Blockierter Aufrufstapel
Hier ist ein Beispiel für das Blockieren des JS-Threads. Lassen Sie uns versuchen, eine foo Datei und eine bar unter Verwendung der Knotenpunkt.js synchrone Funktion readFileSync.
Dies ist ein GIF mit Schleife. Wie Sie sehen, wartet die JS-Engine, bis der erste Aufruf in readFileSync abgeschlossen ist. Dies wird jedoch nicht geschehen, weil es keine foo Datei, so dass die zweite Funktion nie aufgerufen wird.
Asynchrones Verhalten
JS kann jedoch auch nicht blockierend sein und sich so verhalten, als ob es ein Multi-Thread wäre. Das bedeutet, dass es nicht auf die Antwort eines API-Aufrufs, E/A-Ereignisse usw. wartet und die Codeausführung fortsetzen kann. Dies ist dank der JS-Engines möglich, die (unter der Haube) echte Multi-Threading-Sprachen verwenden, wie C++ (Chrome) oder Rust (Firefox). Sie stellen uns die Web-API unter der Browserhaube oder z. B. die I/O API unter Node.js.
Im obigen GIF sehen wir, dass die erste Funktion auf den Aufrufstapel geschoben wird und Hallo wird sofort in der Konsole ausgeführt.
Dann rufen wir die setTimeout Funktion, die von der WebAPI des Browsers bereitgestellt wird. Sie geht an den Aufrufstapel und seinen asynchronen Rückruf foo Funktion geht an die Warteschlange der WebApi, wo sie auf den Aufruf wartet, der nach 3 Sekunden erfolgen soll.
In der Zwischenzeit setzt das Programm den Code fort und wir sehen Hallo. Ich bin nicht blockiert in der Konsole.
Nach dem Aufruf geht jede Funktion in der WebAPI-Warteschlange in die Rückruf-Warteschlange. Hier warten die Funktionen, bis der Aufrufstapel leer ist. Wenn dies geschieht, werden sie nacheinander dorthin verschoben.
Also, wenn unser setTimeout Timer den Countdown beendet, wird unser foo Funktion geht zur Callback-Warteschlange, wartet, bis der Aufrufstapel verfügbar wird, geht dorthin, wird ausgeführt und wir sehen Hallo vom asynchronen Rückruf in der Konsole.
Ereignis-Schleife
Die Frage ist, woher die Laufzeitumgebung weiß, dass der Aufrufstapel leer ist, und wie das Ereignis in der Callback-Warteschlange aufgerufen wird. Das ist die Ereignisschleife. Sie ist ein Teil der JS-Engine. Dieser Prozess prüft ständig, ob der Aufrufstapel leer ist, und wenn ja, überwacht er, ob sich ein Ereignis in der Callback-Warteschlange befindet, das darauf wartet, aufgerufen zu werden.
Das ist der ganze Zauber hinter den Kulissen!
Abschließende Betrachtung der Theorie
Gleichzeitigkeit und Parallelität
Gleichzeitigkeit bedeutet, dass mehrere Aufgaben zur gleichen Zeit, aber nicht gleichzeitig ausgeführt werden. Z.B. arbeiten zwei Aufgaben in sich überschneidenden Zeiträumen.
Parallelität bedeutet, dass zwei oder mehr Aufgaben gleichzeitig ausgeführt werden, z. B. mehrere Berechnungen zur gleichen Zeit.
Fäden und Prozesse
Fäden sind eine Folge von Codeausführungen, die unabhängig voneinander ausgeführt werden können.
Prozess ist eine Instanz eines laufenden Programms. Ein Programm kann mehrere Prozesse haben.
Synchron und asynchron
Unter synchron Bei der Programmierung werden die Aufgaben nacheinander ausgeführt. Jede Aufgabe wartet, bis die vorherige Aufgabe abgeschlossen ist, und wird erst dann ausgeführt.
Unter asynchrone Programmierung können Sie nach der Ausführung einer Aufgabe zu einer anderen Aufgabe wechseln, ohne auf den Abschluss der vorherigen Aufgabe zu warten.
Synchron und asynchron in einer Single- und Multithreading-Umgebung
Synchron mit einem einzigen Thread: Die Aufgaben werden nacheinander ausgeführt. Jede Aufgabe wartet darauf, dass die vorherige Aufgabe ausgeführt wird.
Synchron mit mehreren Threads: Die Aufgaben werden in verschiedenen Threads ausgeführt, warten aber auf andere Aufgaben, die in einem anderen Thread ausgeführt werden.
Asynchron mit einem einzigen Thread: Aufgaben werden ausgeführt, ohne auf die Beendigung einer anderen Aufgabe zu warten. Zu einem bestimmten Zeitpunkt kann nur eine einzige Aufgabe ausgeführt werden.
Asynchron mit mehreren Threads: Aufgaben werden in verschiedenen Threads ausgeführt, ohne auf die Fertigstellung anderer Aufgaben zu warten, und beenden ihre Ausführung unabhängig voneinander.
JavaScript Klassifizierung
Wenn wir uns ansehen, wie JS-Motoren unter der Haube arbeiten, können wir JS als eine asynchrone und interpretierte Single-Thread-Sprache klassifizieren. Das Wort "interpretiert" ist sehr wichtig, denn es bedeutet, dass die Sprache immer laufzeitabhängig und nie so schnell wie kompilierte Sprachen mit integriertem Multithreading sein wird.
Es ist bemerkenswert, dass Node.js echtes Multithreading erreichen kann, vorausgesetzt, dass jeder Thread als separater Prozess gestartet wird. Dafür gibt es Bibliotheken, aber Node.js hat eine eingebaute Funktion namens Arbeitsthemen.
Alle Ereignisschleifen-GIFs stammen aus dem Lupe Anwendung von Philip Roberts, mit der Sie Ihre asynchronen Szenarien testen können.