JavaScript hat ein Nebenläufigkeitsmodell, auf Basis einer Eventschleife. Dieses Modell unterscheidet sich stark von Modellen aus anderer Sprachen wie C und Java.

Laufzeitkonzepte

Der folgenden Abschnitt erklären ein theoretisches Model. Moderne JavaScript-Engines implementieren und optimieren die beschriebenen Semantiken sehr stark.

Visuelle Repräsentation

Stack, heap, queue

Stack

Funktionsaufrufe von einem Stack von Frames.

function foo(b){
  var a = 10;
  return a + b + 11;
}

function bar(x){
  var y = 3;
  return foo(x * y);
}

console.log(bar(7)); // gibt 42 zurück

Beim Aufruf von bar wird ein erster Frame erstellt, der die Argumente und lokalen Variablen von bar enthält. Sobald bar die Funktion foo aufruft, wird ein zweiter Frame erstellt, der auf den ersten gelegt wird und die Argumente und lokalen Variablen von foo enthält. Wenn foo beendet wird, wird der der oberste Frame aus dem Stack entfernt (nur der bar Frame bleibt auf dem Stack). Wenn bar beendet wird, ist der Stack leer.

Heap

Objekte werden in einem Heap gespeichert, welcher hauptsächlich eine meist großer unstrukturierte Region im Speicher ist.

Queue

Eine JavaScript Laufzeitumgebung benutzt eine Nachrichten-Queue, welche eine Liste von Nachrichten ist, die Ausgeführt werden. Jede Nachricht hat eine Funktion die Aufgerufen wird, um die Nachricht abzuarbeiten.

An diesem Punkt, währende der Eventschleife, beginnt die Laufzeitumgebung mit der ältesten Nachricht. Dazu wird die Nachricht aus der Queue entfernt und die zugehörige Funktion mit der Nachricht als Eingabeparameter aufgerufen. Wie immer erzeugt das Aufrufen einer Funktion einen neuen Frame auf dem Stack, für den Funktionsgebrauch.

Die Ausführung von Funktionen geht so lange weiter, bis der Stack wieder leer ist; dann wird die Eventschleife die nächste Nachricht ausführen (wenn eine vorhanden ist).

Eventschleife

Die Eventschleife hat ihren Namen durch die Art, wie dieser meist implementiert wird, was normalerweise dieser ähnelt:

while (queue.waitForMessage()){
  queue.processNextMessage();
}

queue.waitForMessage() wartet synchron auf eine eingehende Nachricht, wenn keine vorhanden ist.

Ausführungsfertigstellung

Jede Nachricht wird vollständig abgearbeitet bevor eine andere Nachricht verarbeitet wird. Dies bietet ein paar schöne Eigenschaften bei der Planung eines Programms, eingeschlossen, das 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 (und der die Daten ändert). Dies unterscheidet sich z. B. von C, wo eine Funktion in einem Thread läuft und gestoppt werden kann, um anderen Code in einem anderen Thread auszuführen.

Dies bringt aber den Nachteil mit sich, dass während Nachricht, die sehr lange dauern, keine Nutzer-Interaktionen, wie z.B. Klicken oder Scrollen, funktioniert. Der Browser entschärft dies mit dem "Ein Skript antwortet nicht mehr" Dialog. Gute Praxis ist es daher die Dauer der Nachrichten kurz zu halten und wenn möglich in 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, geht das Event verloren. So wir z. B. ein Klick auf ein Element mit einem Klicklistener eine Nachricht erzeugen, wie jeder andere Event auch.

Die Funktion setTimeout wird mit 2 Argumenten aufgerufen: eine Nachricht an, die der Queue hinzugefügt wird, und einen Zeitwert (optional; Standard auf 0). Der Zeitwert gibt die (minimale) Zeit an, nach der die Nachricht zur Queue hinzugefügt wird. Die Nachricht wird nach dieser Zeit aufgeführt, wenn keine anderen Nachrichten ausgeführt werden. Aus diesem Grund ist der zweite Parameter die minimale Zeit und garantiert diese nicht.

Hier ist ein Beispiel, welches das Konzept demonstriert (setTimeout wird nicht direkt nach dem Ablaufen der Zeit aufgeführt):

const s = new Date().getSeconds();

setTimeout(function() {
  // prints out "2", meaning that the callback is not called immediately after 500 milliseconds.
  console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);

while(true) {
  if(new Date().getSeconds() - s >= 2) {
    console.log("Good, looped for 2 seconds");
    break;
  }
}

Nullverzögerungen

Nullverzögerungen bedeuten nicht, dass der Aufruf nach null Millisekunden gefeuert wird. Der Aufruf von setTimeout mit einer Verzögerung von 0 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 Nachricht ''this is just a message'' vor der Ausführung des Callback auf der Console ausgegeben, weil die Verzögerung die minimale Verzögerung für die Laufzeigumgebung ist, aber diese nicht garantiert.

Im Grunde muss setTimeout warten, bis der gesamte Code für Nachrichten in der Warteschlange abgeschlossen wurde, obwohl ein bestimmtes Zeitlimit für setTimeout festgelegt wurde.

(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');

})();

// "this is the start"
// "this is just a message"
// "this is the end"
// note that function return, which is undefined, happens here 
// "this is a msg from call back"
// "this is a msg from call back1"

Komunikation mehrere Laufzeitumgebungen zur gleichen Zeit

Ein Web Worker oder ein Cross-Origin iFrame hat seinen eigenen Stack, Heap und Nachrichten Queue. Zwei unabhängige Laufzeitumgebungen 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.

Niemals blockierend

Eine sehr interessante Eigenschaft des Eventschleifen Modells ist, dass Javascript, im Gegensatz zu vielen anderen Sprachen, niemals blockiert. Die Handhabung von I/O wird typischerweise über Events und Callback-Funktionen erledigt, so dass wenn die Applikation auf Ergebnis einer IndexedDB- oder XHR-Anfrage wartet, weitere Benutzereingaben verarbeitet werden können.

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).

Schlagwörter des Dokuments und Mitwirkende

Zuletzt aktualisiert von: schlagi123,