Concurrency model and Event Loop

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

Laufzeitkonzepte

Der folgenden Abschnitt erkl├Ąrt ein theoretisches Modell. 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├če 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├Ąhrend 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 ist nach der Art und Weise, wie diese meist implementiert ist, benannt. Dies ├Ąhnelt meist der folgenden Struktur:

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

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

Ausf├╝hrungsfertigstellung

Jede Nachricht wird vollst├Ąndig verarbeitet, bevor eine andere Nachricht verarbeitet wird. Dies bietet einige nette Eigenschaften zum Verst├Ąndnis der Ausf├╝hrung ihres Programms, einschlie├člich der Tatsache, dass eine Funktion, wann immer sie ausgef├╝hrt wird, nicht vorzeitig verlassen werden kann. Die Funktion wird daher vollst├Ąndig ausgef├╝hrt bevor irgendein anderer Code ausgef├╝hrt wird (und die Daten ├Ąndern k├Ânnte). 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 einer Nachricht, die sehr lange dauern kann, keine Nutzer-Interaktionen, wie z.B. Klicken oder Scrollen, m├Âglich ist. 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. Ein Klick auf ein Element mit einem Click-Event-Handler f├╝gt also eine Nachricht hinzu - ├Ąhnlich wie bei jedem anderen Ereignis.

Die Funktion setTimeout wird mit 2 Argumenten aufgerufen: eine Nachricht, die der Queue hinzugef├╝gt wird, und einen Zeitwert (optional; Standardwert 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 und nicht die garantierte Zeit.

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├Âgerung bedeutet 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 Konsole ausgegeben, weil die Verz├Âgerung die minimale Verz├Âgerung f├╝r die Laufzeigumgebung ist, aber diese nicht garantiert werden kann.

Im Grunde muss setTimeout warten, bis der gesamte Code f├╝r Nachrichten in der Warteschlange abgeschlossen wurde, obwohl ein bestimmtes Zeitlimit f├╝r setTimeout angegeben 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 die Applikation, w├Ąhrend sie auf Ergebnisse einer IndexedDB- oder XHR-Anfrage wartet, weitere Benutzereingaben verarbeiten kann.

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 eher Bugs bei der Implementierung als irgendetwas anderes).