Dieses Kapitel gibt einen Überblick darüber, wie Node.js funktioniert.
Das folgende Diagramm gibt einen Überblick darüber, wie Node.js strukturiert ist.
Die APIs, die einer Node.js-App zur Verfügung stehen, bestehen aus
fetch und CompressionStream fallen in diese Kategorie.process.'node:path' (Funktionen und Konstanten zur Bearbeitung von Dateisystempfaden) und 'node:fs' (Funktionalität im Zusammenhang mit dem Dateisystem).Die Node.js-APIs sind teilweise in JavaScript, teilweise in C++ implementiert. Letzteres ist notwendig, um mit dem Betriebssystem zu interagieren.
Node.js führt JavaScript über eine eingebettete V8-JavaScript-Engine aus (dieselbe Engine, die auch vom Chrome-Browser von Google verwendet wird).
Hier sind einige Highlights von Nodes globalen Variablen.
crypto gibt uns Zugriff auf eine webkompatible Krypto-API.
console überschneidet sich stark mit der gleichen globalen Variable in Browsern (console.log() usw.).
fetch() ermöglicht uns die Nutzung von der Fetch-Browser-API.
process enthält eine Instanz der Klasse Process und gibt uns Zugriff auf Kommandozeilenargumente, Standardeingabe, Standardausgabe und mehr.
structuredClone() ist eine browserkompatible Funktion zum Klonen von Objekten.
URL ist eine browserkompatible Klasse zur Bearbeitung von URLs.
Weitere globale Variablen werden in diesem Kapitel erwähnt.
Die folgenden integrierten Module bieten Alternativen zu globalen Variablen.
'node:console' ist eine Alternative zur globalen Variable console.
console.log('Hello!');
import {log} from 'node:console';
log('Hello!');'node:process' ist eine Alternative zur globalen Variable process.
console.log(process.argv);
import {argv} from 'node:process';
console.log(process.argv);Grundsätzlich ist die Verwendung von Modulen sauberer als die Verwendung globaler Variablen. Da jedoch die Verwendung der globalen Variablen console und process so etablierte Muster sind, hat eine Abweichung davon auch Nachteile.
Die meisten Node-APIs werden über Module bereitgestellt. Dies sind einige häufig verwendete (in alphabetischer Reihenfolge):
'node:assert/strict': Assertions sind Funktionen, die prüfen, ob eine Bedingung erfüllt ist, und bei Nichterfüllung einen Fehler melden. Sie können im Anwendungscode und für Unit-Tests verwendet werden. Dies ist ein Beispiel für die Verwendung dieser API.
import * as assert from 'node:assert/strict';
assert.equal(3 + 4, 7);
assert.equal('abc'.toUpperCase(), 'ABC');
assert.deepEqual({prop: true}, {prop: true}); // deep comparison
assert.notEqual({prop: true}, {prop: true}); // shallow comparison'node:child_process' dient zum synchronen Ausführen nativer Befehle oder in separaten Prozessen. Dieses Modul wird in §12 „Ausführen von Shell-Befehlen in Kindprozessen“ beschrieben.
'node:fs' bietet Dateisystemoperationen wie das Lesen, Schreiben, Kopieren und Löschen von Dateien und Verzeichnissen. Weitere Informationen finden Sie in §8 „Arbeiten mit dem Dateisystem unter Node.js“.
'node:os' enthält betriebssystemspezifische Konstanten und Hilfsfunktionen. Einige davon werden in §7 „Arbeiten mit Dateisystempfaden und Datei-URLs unter Node.js“ erklärt.
'node:path' ist eine plattformübergreifende API zur Arbeit mit Dateisystempfaden. Sie wird in §7 „Arbeiten mit Dateisystempfaden und Datei-URLs unter Node.js“ beschrieben.
'node:stream' enthält eine Node.js-spezifische Streams-API, die in §9 „Native Node.js Streams“ erklärt wird.
'node:util' enthält verschiedene Hilfsfunktionen.
util.parseArgs() wird in §16 „Parsen von Kommandozeilenargumenten mit util.parseArgs()“ beschrieben.Das Modul 'node:module' enthält die Funktion builtinModules(), die ein Array mit den Spezifikationen aller integrierten Module zurückgibt.
import * as assert from 'node:assert/strict';
import {builtinModules} from 'node:module';
// Remove internal modules (whose names start with underscores)
const modules = builtinModules.filter(m => !m.startsWith('_'));
modules.sort();
assert.deepEqual(
modules.slice(0, 5),
[
'assert',
'assert/strict',
'async_hooks',
'buffer',
'child_process',
]
);In diesem Abschnitt verwenden wir folgenden Import:
import * as fs from 'node:fs';Nodes Funktionen gibt es in drei verschiedenen Stilen. Betrachten wir das integrierte Modul 'node:fs' als Beispiel:
Die drei gerade gesehenen Beispiele zeigen die Namenskonvention für Funktionen mit ähnlicher Funktionalität.
fs.readFile().fsPromises.readFile().fs.readFileSync().Werfen wir einen genaueren Blick darauf, wie diese drei Stile funktionieren.
Synchrone Funktionen sind am einfachsten – sie geben sofort Werte zurück und werfen Fehler als Ausnahmen.
try {
const result = fs.readFileSync('/etc/passwd', {encoding: 'utf-8'});
console.log(result);
} catch (err) {
console.error(err);
}Promise-basierte Funktionen geben Promises zurück, die mit Ergebnissen erfüllt und mit Fehlern abgelehnt werden.
import * as fsPromises from 'node:fs/promises'; // (A)
try {
const result = await fsPromises.readFile(
'/etc/passwd', {encoding: 'utf-8'});
console.log(result);
} catch (err) {
console.error(err);
}Beachten Sie den Modulspezifizierer in Zeile A: Die Promise-basierte API befindet sich in einem anderen Modul.
Promises werden detaillierter in „JavaScript for impatient programmers“ erklärt.
Callback-basierte Funktionen übergeben Ergebnisse und Fehler an Callbacks, die ihre letzten Parameter sind.
fs.readFile('/etc/passwd', {encoding: 'utf-8'},
(err, result) => {
if (err) {
console.error(err);
return;
}
console.log(result);
}
);Dieser Stil wird detaillierter in der Node.js-Dokumentation erklärt.
Standardmäßig führt Node.js allen JavaScript in einem einzigen Thread aus, dem Haupt-Thread. Der Haupt-Thread führt kontinuierlich die Event-Loop aus – eine Schleife, die JavaScript-Chunks ausführt. Jeder Chunk ist ein Callback und kann als kooperativ geplanter Task betrachtet werden. Der erste Task enthält den Code (aus einem Modul oder der Standardeingabe), mit dem wir Node.js starten. Andere Tasks werden normalerweise später hinzugefügt, aufgrund von
Eine erste Annäherung an die Event-Loop sieht so aus:
Das heißt, der Haupt-Thread führt Code aus, der dem folgenden ähnelt:
while (true) { // event loop
const task = taskQueue.dequeue(); // blocks
task();
}Die Event-Loop nimmt Callbacks aus einer Task-Queue und führt sie im Haupt-Thread aus. Das Entnehmen von Tasks blockiert (pausiert den Haupt-Thread), wenn die Task-Queue leer ist.
Wir werden später zwei Themen behandeln:
Warum wird diese Schleife Event-Loop genannt? Viele Tasks werden als Reaktion auf Ereignisse hinzugefügt, z.B. solche, die vom Betriebssystem gesendet werden, wenn Eingabedaten zur Verarbeitung bereitstehen.
Wie werden Callbacks zur Task-Queue hinzugefügt? Dies sind gängige Möglichkeiten:
Der folgende Code zeigt eine asynchrone Callback-basierte Operation in Aktion. Er liest eine Textdatei aus dem Dateisystem:
import * as fs from 'node:fs';
function handleResult(err, result) {
if (err) {
console.error(err);
return;
}
console.log(result); // (A)
}
fs.readFile('reminder.txt', 'utf-8',
handleResult
);
console.log('AFTER'); // (B)Dies ist die Ausgabe:
AFTER
Don’t forget!
fs.readFile() führt den Code, der die Datei liest, in einem anderen Thread aus. In diesem Fall ist der Code erfolgreich und fügt diesen Callback zur Task-Queue hinzu.
() => handleResult(null, 'Don’t forget!')Eine wichtige Regel für die Ausführung von JavaScript-Code in Node.js lautet: Jeder Task wird bis zum Ende ausgeführt („läuft bis zum Abschluss“), bevor andere Tasks ausgeführt werden. Das sehen wir im vorherigen Beispiel: 'NACH' in Zeile B wird protokolliert, bevor das Ergebnis in Zeile A protokolliert wird, da der ursprüngliche Task abgeschlossen ist, bevor der Task mit dem Aufruf von handleResult() ausgeführt wird.
Bis zum Abschluss ausführen bedeutet, dass die Lebensdauern von Tasks nicht überlappen und wir uns keine Sorgen machen müssen, dass gemeinsam genutzte Daten im Hintergrund geändert werden. Das vereinfacht Node.js-Code. Das nächste Beispiel demonstriert dies. Es implementiert einen einfachen HTTP-Server:
// server.mjs
import * as http from 'node:http';
let requestCount = 1;
const server = http.createServer(
(_req, res) => { // (A)
res.writeHead(200);
res.end('This is request number ' + requestCount); // (B)
requestCount++; // (C)
}
);
server.listen(8080);Wir führen diesen Code über node server.mjs aus. Danach startet der Code und wartet auf HTTP-Anfragen. Wir können sie senden, indem wir mit einem Webbrowser zu https://:8080 gehen. Jedes Mal, wenn wir diese HTTP-Ressource neu laden, ruft Node.js den Callback auf, der in Zeile A beginnt. Er liefert eine Nachricht mit dem aktuellen Wert der Variablen requestCount (Zeile B) und inkrementiert sie (Zeile C).
Jeder Aufruf des Callbacks ist ein neuer Task und die Variable requestCount wird zwischen Tasks gemeinsam genutzt. Durch das Ausführen bis zum Abschluss ist sie leicht zu lesen und zu aktualisieren. Es ist keine Synchronisation mit anderen gleichzeitig laufenden Tasks erforderlich, da es keine gibt.
Warum läuft Node.js-Code standardmäßig in einem einzigen Thread (mit einer Event-Loop)? Das hat zwei Vorteile:
Wie wir bereits gesehen haben, ist die gemeinsame Nutzung von Daten zwischen Tasks einfacher, wenn es nur einen einzigen Thread gibt.
In traditionellem Multi-Threaded-Code blockiert eine Operation, die lange dauert, den aktuellen Thread, bis die Operation abgeschlossen ist. Beispiele für solche Operationen sind das Lesen einer Datei oder die Verarbeitung von HTTP-Anfragen. Die Durchführung vieler dieser Operationen ist kostspielig, da jedes Mal ein neuer Thread erstellt werden muss. Mit einer Event-Loop sind die Kosten pro Operation geringer, insbesondere wenn jede Operation nicht viel tut. Deshalb können Event-Loop-basierte Webserver höhere Lasten bewältigen als Thread-basierte.
Da einige von Nodes asynchronen Operationen in anderen Threads als dem Haupt-Thread ausgeführt werden (dazu bald mehr) und über die Task-Queue an JavaScript zurückmelden, ist Node.js nicht wirklich Single-Threaded. Stattdessen verwenden wir einen einzigen Thread, um Operationen zu koordinieren, die gleichzeitig und asynchron (im Haupt-Thread) ausgeführt werden.
Damit ist unser erster Blick auf die Event-Loop abgeschlossen. Sie können den Rest dieses Abschnitts überspringen, wenn eine oberflächliche Erklärung für Sie ausreicht. Lesen Sie weiter, um mehr Details zu erfahren.
Die echte Event-Loop hat mehrere Task-Queues, aus denen sie in mehreren Phasen liest (Sie können sich einen Teil des JavaScript-Codes im GitHub-Repository nodejs/node ansehen). Das folgende Diagramm zeigt die wichtigsten dieser Phasen:
Was machen die im Diagramm gezeigten Event-Loop-Phasen?
Die Phase „timers“ ruft zeitgesteuerte Tasks auf, die von ihr mit folgendem hinzugefügt wurden:
setTimeout(task, delay=1) führt den Callback task nach delay Millisekunden aus.setInterval(task, delay=1) führt den Callback task wiederholt aus, mit Pausen von delay Millisekunden.Die Phase „poll“ ruft I/O-Ereignisse ab und verarbeitet sie und führt I/O-bezogene Tasks aus ihrer Queue aus.
Die Phase „check“ (die „immediate Phase“) führt Tasks aus, die über folgendes geplant wurden:
setImmediate(task) führt den Callback task so schnell wie möglich aus („sofort“ nach Phase „poll“).Jede Phase läuft, bis ihre Queue leer ist oder bis eine maximale Anzahl von Tasks verarbeitet wurde. Mit Ausnahme von „poll“ wartet jede Phase bis zu ihrer nächsten Runde, bevor sie Tasks verarbeitet, die während ihres Laufs hinzugefügt wurden.
setImmediate()-Tasks gibt, geht die Verarbeitung zur Phase „check“ über.Wenn diese Phase länger als ein systemabhängiges Zeitlimit dauert, endet sie und die nächste Phase wird ausgeführt.
Nach jedem ausgeführten Task läuft eine „Sub-Schleife“, die aus zwei Phasen besteht:
Die Sub-Phasen bearbeiten:
process.nextTick() enqueued werden.queueMicrotask(), Promise-Reaktionen usw. enqueued werden.Next-tick-Aufgaben sind Node.js-spezifisch, Microtasks sind ein plattformübergreifender Webstandard (siehe MDNs Kompatibilitätstabelle).
Diese Sub-Schleife läuft, bis beide Queues leer sind. Tasks, die während ihres Laufs hinzugefügt werden, werden sofort verarbeitet – die Sub-Schleife wartet nicht bis zu ihrer nächsten Runde.
Wir können die folgenden Funktionen und Methoden verwenden, um Callbacks zu einer der Task-Queues hinzuzufügen:
setTimeout() (Webstandard)setInterval() (Webstandard)setImmediate() (Node.js-spezifisch)process.nextTick() (Node.js-spezifisch)queueMicrotask(): (Webstandard)Es ist wichtig zu beachten, dass wir bei der Zeitplanung einer Aufgabe über eine Verzögerung die frühestmögliche Zeit angeben, zu der die Aufgabe ausgeführt wird. Node.js kann sie nicht immer genau zur geplanten Zeit ausführen, da es nur zwischen Tasks prüfen kann, ob zeitgesteuerte Tasks fällig sind. Daher kann eine lang laufende Aufgabe dazu führen, dass zeitgesteuerte Tasks verspätet ausgeführt werden.
Betrachten Sie den folgenden Code
function enqueueTasks() {
Promise.resolve().then(() => console.log('Promise reaction 1'));
queueMicrotask(() => console.log('queueMicrotask 1'));
process.nextTick(() => console.log('nextTick 1'));
setImmediate(() => console.log('setImmediate 1')); // (A)
setTimeout(() => console.log('setTimeout 1'), 0);
Promise.resolve().then(() => console.log('Promise reaction 2'));
queueMicrotask(() => console.log('queueMicrotask 2'));
process.nextTick(() => console.log('nextTick 2'));
setImmediate(() => console.log('setImmediate 2')); // (B)
setTimeout(() => console.log('setTimeout 2'), 0);
}
setImmediate(enqueueTasks);Wir verwenden setImmediate(), um eine Besonderheit von ESM-Modulen zu vermeiden: Sie werden in Microtasks ausgeführt, was bedeutet, dass, wenn wir Microtasks auf der obersten Ebene eines ESM-Moduls enqueuen, sie vor Next-tick-Tasks ausgeführt werden. Wie wir gleich sehen werden, ist das in den meisten anderen Kontexten anders.
Dies ist die Ausgabe des vorherigen Codes:
nextTick 1
nextTick 2
Promise reaction 1
queueMicrotask 1
Promise reaction 2
queueMicrotask 2
setTimeout 1
setTimeout 2
setImmediate 1
setImmediate 2
Beobachtungen
Alle Next-tick-Tasks werden unmittelbar nach enqueueTasks() ausgeführt.
Ihnen folgen alle Microtasks, einschließlich Promise-Reaktionen.
Die Phase „timers“ folgt auf die Immediate-Phase. Dann werden die zeitgesteuerten Aufgaben ausgeführt.
Wir haben Immediate-Tasks während der Immediate-Phase („check“) hinzugefügt (Zeile A und Zeile B). Sie erscheinen zuletzt in der Ausgabe, was bedeutet, dass sie nicht während der aktuellen Phase, sondern während der nächsten Immediate-Phase ausgeführt wurden.
Der folgende Code untersucht, was passiert, wenn wir eine Next-tick-Aufgabe während der Next-tick-Phase und eine Microtask während der Microtask-Phase enqueuen:
setImmediate(() => {
setImmediate(() => console.log('setImmediate 1'));
setTimeout(() => console.log('setTimeout 1'), 0);
process.nextTick(() => {
console.log('nextTick 1');
process.nextTick(() => console.log('nextTick 2'));
});
queueMicrotask(() => {
console.log('queueMicrotask 1');
queueMicrotask(() => console.log('queueMicrotask 2'));
process.nextTick(() => console.log('nextTick 3'));
});
});Dies ist die Ausgabe:
nextTick 1
nextTick 2
queueMicrotask 1
queueMicrotask 2
nextTick 3
setTimeout 1
setImmediate 1
Beobachtungen
Next-tick-Aufgaben werden zuerst ausgeführt.
„nextTick 2“ wird während der Next-tick-Phase enqueued und sofort ausgeführt. Die Ausführung wird erst fortgesetzt, wenn die Next-tick-Queue leer ist.
Dasselbe gilt für Microtasks.
Wir enqueuen „nextTick 3“ während der Microtask-Phase und die Ausführung springt zurück zur Next-tick-Phase. Diese Sub-Phasen werden wiederholt, bis beide Queues leer sind. Erst dann geht die Ausführung zu den nächsten globalen Phasen über: Zuerst die „timers“-Phase („setTimeout 1“). Dann die Immediate-Phase („setImmediate 1“).
Der folgende Code untersucht, welche Arten von Tasks Event-Loop-Phasen aushungern (sie durch unendliche Rekursion am Laufen hindern) können:
import * as fs from 'node:fs/promises';
function timers() { // OK
setTimeout(() => timers(), 0);
}
function immediate() { // OK
setImmediate(() => immediate());
}
function nextTick() { // starves I/O
process.nextTick(() => nextTick());
}
function microtasks() { // starves I/O
queueMicrotask(() => microtasks());
}
timers();
console.log('AFTER'); // always logged
console.log(await fs.readFile('./file.txt', 'utf-8'));Die Phasen „timers“ und „immediate“ führen keine Tasks aus, die während ihrer Phasen enqueued werden. Deshalb hungern timers() und immediate() fs.readFile() nicht aus, das während der „poll“-Phase zurückmeldet (es gibt auch eine Promise-Reaktion, aber ignorieren wir diese hier).
Aufgrund der Art und Weise, wie Next-tick-Aufgaben und Microtasks geplant werden, verhindern sowohl nextTick() als auch microtasks() die Ausgabe in der letzten Zeile.
Am Ende jeder Iteration der Event-Loop prüft Node.js, ob es Zeit zum Beenden ist. Es führt einen Referenzzähler für ausstehende Timeouts (für zeitgesteuerte Aufgaben).
setImmediate(), setInterval() oder setTimeout() erhöht den Referenzzähler.Wenn der Referenzzähler am Ende einer Event-Loop-Iteration Null ist, beendet sich Node.js.
Das sehen wir im folgenden Beispiel:
function timeout(ms) {
return new Promise(
(resolve, _reject) => {
setTimeout(resolve, ms); // (A)
}
);
}
await timeout(3_000);Node.js wartet, bis das von timeout() zurückgegebene Promise erfüllt ist. Warum? Weil die Aufgabe, die wir in Zeile A planen, die Event-Loop am Leben erhält.
Im Gegensatz dazu erhöht das Erstellen von Promises nicht den Referenzzähler:
function foreverPending() {
return new Promise(
(_resolve, _reject) => {}
);
}
await foreverPending(); // (A)In diesem Fall verlässt die Ausführung diese (Haupt-)Task während await in Zeile A vorübergehend. Am Ende der Event-Loop ist der Referenzzähler Null und Node.js wird beendet. Allerdings ist die Beendigung nicht erfolgreich. Das heißt, der Exit-Code ist nicht 0, sondern 13 („Unfinished Top-Level Await“).
Wir können manuell steuern, ob ein Timeout die Event-Loop am Leben erhält: Standardmäßig halten Tasks, die über setImmediate(), setInterval() und setTimeout() geplant werden, die Event-Loop so lange am Leben, wie sie ausstehen. Diese Funktionen geben Instanzen der Klasse Timeout zurück, deren Methode .unref() diese Standardeinstellung ändert, sodass das aktive Timeout Node.js nicht am Beenden hindert. Methode .ref() stellt die Standardeinstellung wieder her.
Tim Perry erwähnt einen Anwendungsfall für .unref(): Seine Bibliothek verwendete setInterval(), um wiederholt eine Hintergrundaufgabe auszuführen. Diese Aufgabe verhinderte, dass Anwendungen beendet wurden. Er löste das Problem über .unref().
libuv ist eine in C geschriebene Bibliothek, die viele Plattformen (Windows, macOS, Linux usw.) unterstützt. Node.js verwendet sie zur Handhabung von I/O und mehr.
Netzwerk-I/O ist asynchron und blockiert nicht den aktuellen Thread. Solche I/O umfasst:
Zur Handhabung asynchroner I/O verwendet libuv native Kernel-APIs und abonniert I/O-Ereignisse (epoll unter Linux; kqueue unter BSD Unix inkl. macOS; Event Ports unter SunOS; IOCP unter Windows). Es erhält dann Benachrichtigungen, wenn diese auftreten. All diese Aktivitäten, einschließlich der I/O selbst, finden auf dem Haupt-Thread statt.
Einige native I/O-APIs sind blockierend (nicht asynchron) – zum Beispiel Dateie/o und einige DNS-Dienste. libuv ruft diese APIs aus Threads in einem Thread-Pool (dem sogenannten „Worker-Pool“) auf. Dies ermöglicht es dem Haupt-Thread, diese APIs asynchron zu verwenden.
libuv hilft Node.js bei mehr als nur I/O. Weitere Funktionalitäten umfassen:
Nebenbei bemerkt, hat libuv seine eigene Event-Loop, deren Quellcode Sie im GitHub-Repository libuv/libuv (Funktion uv_run()) einsehen können.
Wenn wir möchten, dass Node.js auf I/O reagiert, sollten wir langlaufende Berechnungen in Haupt-Thread-Tasks vermeiden. Es gibt zwei Optionen dafür:
setImmediate() ausführen. Dies ermöglicht es der Event-Loop, I/O zwischen den Teilen durchzuführen.Die nächsten Unterabschnitte behandeln einige Optionen zur Auslagerung.
Worker Threads implementieren die plattformübergreifende Web Workers API mit einigen Unterschieden – z.B.:
Worker Threads müssen aus einem Modul importiert werden, Web Workers werden über eine globale Variable angesprochen.
Innerhalb eines Workers wird das Lauschen auf Nachrichten und das Senden von Nachrichten über Methoden des globalen Objekts in Browsern gehandhabt. In Node.js importieren wir stattdessen parentPort.
Wir können die meisten Node.js-APIs von Workern aus verwenden. In Browsern ist unsere Auswahl begrenzter (wir können das DOM usw. nicht verwenden).
Unter Node.js sind mehr Objekte übertragbar (alle Objekte, deren Klassen von der internen Klasse JSTransferable erben) als in Browsern.
Einerseits sind Worker Threads wirklich Threads: Sie sind leichter als Prozesse und laufen im selben Prozess wie der Haupt-Thread.
Andererseits:
Atomics bietet atomare Operationen und Synchronisationsprimitive, die bei der Verwendung von SharedArrayBuffers hilfreich sind.Weitere Informationen finden Sie in der Node.js-Dokumentation zu Worker Threads.
Cluster ist eine Node.js-spezifische API. Sie ermöglicht uns das Ausführen von Clustern von Node.js-Prozessen, die wir zur Verteilung von Arbeitslasten verwenden können. Die Prozesse sind vollständig isoliert, teilen sich aber Server-Ports. Sie können kommunizieren, indem sie JSON-Daten über Kanäle übergeben.
Wenn wir keine Prozessisolation benötigen, können wir Worker Threads verwenden, die leichter sind.
Child process ist eine weitere Node.js-spezifische API. Sie ermöglicht uns das Erzeugen neuer Prozesse, die native Befehle ausführen (oft über native Shells). Diese API wird in §12 „Ausführen von Shell-Befehlen in Kindprozessen“ behandelt.
Node.js Event Loop
process.nextTick()“Videos zur Event-Loop (die einige Hintergrundkenntnisse auffrischen, die für dieses Kapitel erforderlich sind):
libuv
JavaScript-Nebenläufigkeit