JavaScript hat ein Nebenläufigkeitsmodell (concurrency model), welches auf einem "Event Loop" basiert. Dieses Modell unterscheidet sich stark von Modellen anderer Sprachen wie C oder Java.
Laufzeitkonzepte
Der folgenden Abschnitt erklären ein theoretisches Model. Moderne JavaScript-Engines implementieren und optimieren die beschriebenen Semantiken stark.
Visuelle Repräsentation
Stack
Funktionsaufrufe von einem Stack von Frames.
function f(b){ var a = 12; return a+b+35; } function g(x){ var m = 4; return f(m*x); } g(21);
Beim Aufruf der Funktion g wird ein erste Frame erstellt, der die Argumente und lokalen Variablen von g enthält. Sobald g die Funktion f aufruft, wird ein zweiter Frame erstellt, der auf den ersten gelegt wird. Dieser enthhält die Argumente und lokalen Variablen von f. Wenn f beendet wird, wird der der oberste Frame aus dem Stack entfernt (nur der g Frame bleibt auf dem Stack). Wenn g beendet wird, ist der Stack leer.
Heap
Objekte werden in einem Heap reserviert. Als Heap wird eine hauptsächlich unstrukturierte Region des Speichers bezeichnet.
Queue
Zur Laufzeit enthält JavaScript eine Message Queue (Nachrichtenwarteschlange), welche eine Liste an Nachrichten enthält, die abgearbeitet werden sollen. Jede Nachricht steht mit einer Funktion im Zusammenhang. Wenn der Stack leer ist, wird eine Nachricht aus der Schlange entnommen und verarbeitet. Beim Verarbeiten wird die im Zusammenhang stehende Funktion aufgerufen wodurch ein erstes Stack-Frame erzeugt wird. Die Verarbeitung der Nachricht endet sobald der Stack erneut leer ist.
Event Loop
Der event loop
erhielt seinen Namen vermutlich durch die Art, wie dieser meist implementiert wird (als Wiederholungen):
while (queue.waitForMessage()){ queue.processNextMessage(); }
Sollten zu einem Zeitpunkt keine Nachrichten vorhanden sind, wartet queue.waitForMessage
synchron auf eingehende Nachrichten.
"Run-to-completion" (Ausführen bis zur Fertigstellung)
Jede Nachricht wird vollständig abgearbeitet bevor irgendeine weitere Nachricht verarbeitet wird. Dies bietet ein paar schöne Eigenschaften bei der Planung eines Programms: Wenn eine Funktion ausgeführt wird, kann dieser nichts vorzeitig verlassen werden und die Funktion wird daher vollständig ausgeführt bevor irgendein anderer Code ausgeführt wird, der gegebenenfalls die Daten ändert, die die Funktion manipulieren soll. Dies untescheidet sich z.B. von C, wo eine Funktion in einem Thread läuft und gestoppt werden kann, um anderen anderen Code in einem anderen Thread auszuführen.
Dies bringt aber auch folgenden Nachteil mit sich: Braucht eine Nachricht extrem lange um fertig verarbeitet zu werden, kann währenddessen die Webanwendung auf keine Nutzer-Interaktionen, wie z.B. Klicken oder Scrollen, reagieren. Der Browser entschärft dies mit dem "Ein Skript antwortet nicht mehr" Dialog. Gute Praxis ist es daher die Dauer der Nachrichtenverarbeitung kurz zu halten und wenn möglich in viele einzelne Nachrichten aufzuteilen.
Hinzufügen von Nachrichten
In einem Webbrowser werden Nachrichten immer hinzugefügt, wenn ein Event auftritt und diesem ein Event Listener hinzugefügt wurde. Ist kein Listener vorhanden, so geht das Event verloren. So würde z.B. ein Klick auf ein Element eine Nachricht erzeugen, falls diesem ein Klick-Event Listener hinzugefügt wurde .
Ein Aufruf der Funktion setTimeout
hängt der Queue eine Nachricht an, nachdem die im zweiten Argument angegebene Zeit vergangenen ist. Existiert keine andere Nachricht in der Schlange, so wird die Callback-Funktion unmittelbar ausgeführt; wenn in der Schlange hingegen viele Nachrichten vorhanden sind, wird die setTimeout
Nachricht erst verarbeitet sobald alle vorherigen Nachrichten verarbeitet wurden. Aus diesem Grund gibt das zweite Argument immer nur die Mindestwartezeit und nicht die exakte Wartezeit an.
Nullverzögerungen
Nullverzögerungen bedeuten nicht, dass der Aufruf nach null Millisekunden gefeuert wird. Der Aufruf von setTimeout
mit einer verzögerung von 0 (null) Millisekunden führt die übergebene Funktion nicht nach dem gegebenen Intervall aus. Die Ausführung hängt von der anzahl von wartenden Aufrufen in der Queue ab. Im Beispiel unten, wird die übergebene Funktion ausgeführt, weil die Verzögerung die minimale Zeit, die benötigt wird, um die Ausführung zu starten. Die Verzögerung ist keine garantierte Zeit.
(function () { console.log('this is the start'); setTimeout(function cb() { console.log('this is a msg from call back'); }); console.log('this is just a message'); setTimeout(function cb1() { console.log('this is a msg from call back1'); }, 0); console.log('this is the end'); })();
Mehrere Laufzeitumgebungen zur gleichen Zeit
Ein Web Worker oder ein Cross-Origin iFrame haben ihren eigenen Stack, ihren eigenen Heap und ihre eigene Message Queue. Zwei unabhängige Laufzeiten können nur durch das Senden von nachrichten über die postMessage
Methode kommunizieren. Diese Methode fügt der Message Queue der anderen Laufzeit eine Nachricht hinzu, wenn diese auf die Message-Events hört (listens).
Never blocking (niemals blockieren)
Eine sehr interessante Eigenschaft des Event Loop Modells ist es, dass Javascript, im Gegensatz zu vielen anderen Sprachen, niemals blockierend ist. Die Handhabung von I/O wird typischerweise über Events und Callback-Funktionen erledigt. Dies ermöglicht einer Applikation weiterhin auf andere Dinge, wie z.B. Nutzer-Eingaben, zu reagieren, auch wenn sie selber noch auf das Ergebnis einer IndexedDB- oder XHR-Anfrage wartet.
Es gibt Ausnahmen, wie z.B. alert oder synchrone XHR, wobei es eine gute Praxis ist, diese zu vermeiden. Obacht, es existieren Ausnahmen für die Ausnahme (aber diese sind für gewöhnlich vielmehr Bugs bei der Implementierung als irgendetwas anderes).