Du hast deinen AdBlocker an?

Es wäre ein Traum, wenn du ihn für t3n.de deaktivierst. Wir zeigen dir gerne, wie das geht. Und natürlich erklären wir dir auch, warum uns das so wichtig ist. Digitales High-five, deine t3n-Redaktion

Entwicklung & Design

Event-Loop: So funktioniert die Befehlsverarbeitung in JavaScript

JavaScript-Code. (Foto: t3n)

Dass JavaScript sequentiell, also Befehl für Befehl, abgearbeitet wird, dürfte bekannt sein. Wie genau das Ganze aber funktioniert, hilft dir dabei zu verstehen, wie deine Skripte besser aufgebaut werden können.

JavaScript-Befehle werden von der Browser-Engine mit dem so genannten „Event-Loop“ abgearbeitet. Dabei wird bei JavaScript ein zugrundeliegendes Speichermodell verwendet, bei dem diese drei wichtigen Bereiche zum Einsatz kommen: der Stack, die Queue und der Heap.

JavaScript Speicherbelergung
Moderne JavaScript-Engines verarbeiten die Befehle mit Stack, Queue und Heap. (Grafik: Mozilla Developer Network)

Es gibt verschiedene JavaScript Engines in den verschiedenen Browsern (Chrome hat V8, Firefox hat OdinMonkey) und jeder Browser wird diese Funktionen anders implementieren. Diese grundlegende Erklärung sollte aber für alle gültig sein.

Heap

Der „einfachste“ Teil des Systems ist der so genannte Heap. Aus dem Englischen übersetzt bedeutet Heap schlichtweg „Haufen“ und genauso verhält sich dieser Bereich auch. Der Heap ist ein größerer Speicher, in dem Variablen, Funktionen und Objekte, die du mit deinem JavaScript erzeugst, gespeichert werden.

Verschiedene Browser und JavaScript-Engines führen unterschiedliche Optimierungsmaßnahmen für den Heap durch. Wenn ein Objekt zum Beispiel mehrfach dupliziert wird, können einige Engines dieses Objekt weiterhin nur einmal im Heap behalten und erst dann kopieren, wenn das neue Objekt vom Ursprungsobjekt abweicht.

Stack

Im Stack werden die zur Ausführung benötigten Funktionen abgelegt und verarbeitet. Jede im Stack abgelegte Funktion wird als Frame bezeichnet und beinhaltet Zeiger zum Heap und den jeweils dazugehörigen Objekten.

function f(b){
var a = 12;
return a+b + 35;
}
function g(x){
var m = 4;
return f(m*x);
}
g(21);

Führst du beispielsweise den oben stehenden Code aus, passiert Folgendes:

Sobald g aufgerufen wird, wird ein Frame mit den übergebenen Parametern im Stack abgelegt. Wenn g auf f zugreift, wird ein zweiter Frame mit den Inhalten und Argumenten von f und lokale Variablen oben auf den Stack gelegt. Sobald f verarbeitet wird, wird f aus dem Stack entfernt, sodass nur g übrig bleibt. g wird nun ebenfalls verarbeitet und zurückgegeben – der Stack bleibt leer zurück.

Verschiedene JavaScript Engines haben unter Umständen unterschiedliche Stack-Größen. Sofern du keine rekursiven Funktionen ausführst, ist es jedoch sehr unwahrscheinlich, dass du ihre Grenzen erreichst. Andernfalls verursachst du einen Stack-Overflow und deine Applikation stürzt höchstwahrscheinlich ab.

Queue

Die Queue enthält alle JavaScript-Befehle beziehungsweise Messages, die verarbeitet werden müssen. Wenn du eine Funktion aufrufst, wird sie zusammen mit weiteren dazugehörigen Funktionen als „Message“ gruppiert und im Queue abgelegt. Ist der Stack leer, werden Messages auf der Queue in den Stack verschoben und verarbeitet.

Das gilt auch für timerbasierte Messages. Ein Beispiel: Bei einem Aufruf von setTimeout wird eine Message zur Queue hinzufügt, die nach einer im Funktionsaufruf definierten Zeit ausgeführt wird. Wenn keine andere Message in der Queue liegt, wird die Nachricht sofort nach Ablauf des festgelegten Intervalls ausgeführt. Wenn andere Messages vorher ausgeführt werden müssen, muss die setTimeout-Message warten, sodass der festgelegte Zeitwert nur die Minimum-Zeit und keine Garantie-Zeit bis zur Ausführung definieren kann.

Der Event-Loop

JavaScript-Runtimes beinhalten also eine Message-Queue, in der die abzuarbeitenden Befehle und ihreCallbacks gespeichert werden. Diese werden in sequentieller Reihenfolge (first come, first serve) in der Queue abgelegt, beispielsweise durch User-Interaktion oder XHR-Requests.

In dem Event-Loop werden, wie der Name schon sagt, in einer Schleife die verschiedenen Messages und ihre Callback-Funktionen abgearbeitet – man spricht hier von einem so genannten „Tick“. Der Aufruf der Callback-Funktion generiert den ersten Frame im Stack und hält wegen der Single-Thread-Beschaffenheit von JavaScript sämtliche weitere Aktivitäten des Loop an, bis der Stack abgearbeitet wurde.

Kein Blocken, keine Probleme

Eine sehr interessante Eigenschaft von JavaScript ist, dass – anders als bei vielen anderen Programmiersprachen – bei der Verarbeitung von Input- und Output-Events (I/O) nicht zwangsläufig die weitere Ausführung des Skripts blockiert wird. JavaScript kann das I/O mit Events und Callbacks regeln, ohne die Ausführung des Codes zu verzögern. Wenn eine Applikation also beispielsweise auf die Antwort einer Datenbank, die über XHR angefragt wird, wartet, können weiterhin Messages aus der Queue verarbeitet werden.

Ausnahmen zu der Regel gibt es wie immer. So blocken zum Beispiel Dialoge wie alert oder synchrones XHR die Skript-Ausführung und können zu längeren Verarbeitungszeiten und Blocking führen.

Die Vorteile des Event-Loops bei JavaScript

Jede Message wird komplett verarbeitet, bevor eine weitere Message in den Stack gelegt wird. So kannst du sicher sein, dass dein Code, bis auf die oben genannten Ausnahmen, sequentiell abgearbeitet wird und die Daten nicht durch andere Funktionen verändert werden und zu Fehlfunktionen führen können. Bei Multithread-Anwendungen spricht man bei einem derartigen Fehler von so genannten „Race-Conditions“.

Die Nachteile des Event-Loops bei JavaScript

Der Nachteil ist, dass Messages, die zu lange für die Ausführung benötigen, die WebApp oder Webseite zum Stillstand bringen können. Das kann sich darin äußern, dass der User keine weiteren Interaktionen durchführen und gegebenenfalls nicht mal mehr scrollen kann. Diese Problematik wird dann mit der typischen Fehlermeldung „Ein Skript verlangsamt die Webseite …“ und der Möglichkeit, die Ausführung abzubrechen, quittiert. Ein guter Workaround hierfür ist das Aufteilen von Messages, um eine möglichst schnelle Verarbeitung zu ermöglichen.

Auch kann durch die sequentielle Abarbeitung von Messages keine optimale Performance erzielt werden, da immer auf die Ausführung vorhergehender Messages gewartet werden muss. So dauert eine Aktion im schlimmsten Fall so lange, wie die Verarbeitungszeit aller Messages zusammengenommen – und anders als bei Multi-Threaded-Anwendungen nicht so lange, wie die Verarbeitungszeit der langsamten Message.

Fazit

Webworker haben durch den Event-Loop nicht mit Race-Conditions zu kämpfen, mit denen sich Entwickler von Multithread-Applikationen täglich rumschlagen müssen. Der Event-Loop ist fester Bestandteil von JavaScript und muss nicht erst implementiert werden. Zudem kann durch die Speicherung der Daten im Heap bei jedem weiteren Event darauf zurückgegriffen werden, während man in Programmiersprachen wie PHP auf eine Speicherung der Daten in einer Datenbank zurückgreifen muss.

Nimmt die Applikation aber viele CPU-Ressourcen in Anspruch, kann dieser Ansatz zu Problemen führen, da (Node.js ausgenommen) nur ein Thread für die Verarbeitung zur Verfügung steht. Auch ist die Gefahr von Memory-Leaks gegeben, wenn die Applikation nicht sauber geschrieben ist und bei längerer Laufzeit den Heap mit Datenmüll vollstopft.

Der Event-Loop in JavaScript ist super für I/O-Applikationen und furchtbar für CPU-lastige Anwendungen. Auch wenn jedes Budget-Smartphone mehrere CPU-Kerne zur Verfügung hat, kann nur ein Teil „quasi-parallel“ abgearbeitet werden, während der Rest streng sequentiell verarbeitet werden muss.

Zusatz: JavaScript und Multi Thread – Und es geht doch!

JavaScript ist zwar eine Single-Thread-Applikation, kann aber trotzdem von mehreren Cores Gebrauch machen. Web-Workers heißt hier das Zauberwort, das in den letzten Jahren zum festen Bestandteil von JavaScript-Engines geworden ist und in jedem modernen Browser zur Verfügung steht. Web-Workers ermöglichen deinem Browser, rechenintensive Aufgaben an einen separaten Thread abzugeben und so eine schnellere Verarbeitung zu erzielen.

Hierfür lagerst du den jeweiligen Code in eine Datei aus, die du mit var worker = new Worker('datei.j'‘) laden, mit Event Handlern versehen und so sicherstellen kannst, dass der Worker mit dem Hauptprogramm und anderen Workern interagieren kann. Hierfür wird so genanntes „Message Passing“ eingesetzt, das Informationen mit einfachen JSON-Objekten teilen kann. Auf eine genauere Implementation von Web-Workern kommen wir in einem späteren Artikel zu sprechen.

Bitte beachte unsere Community-Richtlinien

5 Reaktionen
Max Scheffler

Sehr wichtiger Artikel!

Kleine Anmerkungen dazu:

"Wenn du eine Funktion aufrufst, wird sie zusammen mit weiteren dazugehörigen Funktionen als „Message“ gruppiert und im Queue abgelegt." - Das ist so nicht ganz richtig. Nicht "du" rufst eine Funktion auf, sondern ein Event führt zur Ausführung einer damit assoziierten Funktion (Event Callback). Ein solches Event kann ein click, hover oder eben timeout sein. Alle weiteren Funktionen die von dieser ersten aufgerufenen Funktion aufgerufen werden laufen im gleichen Zeitschlitz bzw. Tick ab.

DOM Manipulationen, z.B. das hinzufügen eines DIVs sind genauso Events. Diese werden also erst ausgeführt, wenn die äußerste Funktion terminiert. Ein kleines Beispiel dazu: http://jsfiddle.net/scheffield/2Cr9P/

Antworten
irgendeinem Spinner

Wobei man noch erwähnen sollte, dass das günstigere Webstorm vollkommen reicht, wenn man nur Javascript und kein PHP schreibt.

Ich habe Phpstorm ausgiebieg getestet und fand es durchaus gut, wobei es bei mir nicht gegen Sublime Text anstinken konnte. Ich schreibe hauptsächlich Javascript und da bringt mir die IDE nicht so viel Zusatznutzen.

Antworten
Ilja Zaglov

Hallo Thomas,

der Editor im Bild ist PHPStorm von JetBrains (http://www.jetbrains.com/phpstorm/). Kann mir nicht mehr vorstellen ohne das gute Stück zu arbeiten!

Viele Grüße
Ilja

Antworten
Thomas

hi, könnt ihr mir vielleicht verraten welchen Editor ihr für das Bild verwendet habt? Sieht interessant aus. Danke! Gruß Thomas

Antworten
Michael Schulze

Mit kleinen Tricks kann man man die Performance in JavaScript verbessern und Non-blocking JavaScript schreiben. Ansynchrones JavaScript (kein multithreading) lässt sich über setTimeout- oder setInterval-Funktionen simulieren.
Dazu zwei gute Blog-Beiträge, die das sehr gut erklären:
How JavaScript Timers Work und Run intense JS without freezing the browser

Antworten

Melde dich mit deinem t3n Account an oder fülle die unteren Felder aus.