Die „Ereignisschleife“ des Knotens ist der Kern seiner Fähigkeit, große Parallelität und hohen Durchsatz zu bewältigen. Dies ist der magischste Teil, wonach Node.js grundsätzlich als „Single-Threaded“ verstanden werden kann und gleichzeitig die Verarbeitung beliebiger Vorgänge im Hintergrund ermöglicht. In diesem Artikel wird erläutert, wie die Ereignisschleife funktioniert, damit Sie ihre Magie spüren können.
Ereignisgesteuerte Programmierung
Um die Ereignisschleife zu verstehen, müssen Sie zunächst die ereignisgesteuerte Programmierung verstehen. Es erschien 1960. Heutzutage wird die ereignisgesteuerte Programmierung häufig in der UI-Programmierung verwendet. Eine der Hauptanwendungen von JavaScript ist die Interaktion mit dem DOM, daher ist die Verwendung einer ereignisbasierten API selbstverständlich.
Einfach definiert: Ereignisgesteuerte Programmierung steuert den Ablauf einer Anwendung durch Ereignisse oder Zustandsänderungen. Wird im Allgemeinen durch Ereignisüberwachung implementiert. Sobald das Ereignis erkannt wird (dh sich der Status ändert), wird die entsprechende Rückruffunktion aufgerufen. Kommt Ihnen das bekannt vor? Tatsächlich ist dies das grundlegende Funktionsprinzip der Node.js-Ereignisschleife.
Wenn Sie mit der clientseitigen JavaScript-Entwicklung vertraut sind, denken Sie an die .on*()-Methoden wie element.onclick(), die zur Kombination mit DOM-Elementen verwendet werden, um Benutzerinteraktion bereitzustellen. Dieser Arbeitsmodus ermöglicht das Auslösen mehrerer Ereignisse auf einer einzelnen Instanz. Node.js löst dieses Muster durch EventEmitter (Ereignisgeneratoren) aus, beispielsweise in den serverseitigen Socket- und „http“-Modulen. Eine oder mehrere Zustandsänderungen können von einer einzelnen Instanz aus ausgelöst werden.
Ein weiteres häufiges Muster besteht darin, Erfolg und Misserfolg auszudrücken. Im Allgemeinen gibt es zwei gängige Implementierungsmethoden. Die erste besteht darin, die „Fehlerausnahme“ an den Rückruf zu übergeben, normalerweise als ersten Parameter an die Rückruffunktion. Der zweite verwendet das Promises-Entwurfsmuster und hat ES6 hinzugefügt. Hinweis* Der Promise-Modus verwendet eine Methode zum Schreiben von Funktionsketten ähnlich wie jQuery, um eine tiefe Verschachtelung von Rückruffunktionen zu vermeiden, wie zum Beispiel:
Das Modul „fs“ (Dateisystem) übernimmt größtenteils den Stil der Übergabe von Ausnahmen an Rückrufe. Technisch gesehen löst es bestimmte Aufrufe aus, beispielsweise das angehängte Ereignis fs.readFile(), aber die API dient lediglich dazu, den Benutzer zu warnen und den Erfolg oder Misserfolg des Vorgangs auszudrücken. Die Wahl einer solchen API basiert eher auf architektonischen Überlegungen als auf technischen Einschränkungen.
Ein häufiges Missverständnis besteht darin, dass Ereignisemitter beim Auslösen von Ereignissen ebenfalls von Natur aus asynchron sind. Dies ist jedoch falsch. Unten finden Sie einen einfachen Codeausschnitt, um dies zu demonstrieren.
MyEmitter.prototype.doStuff = function doStuff() {
console.log('before')
emitter.emit('fire')
console.log('after')}
};
var me = new MyEmitter();
me.on('fire', function() {
console.log('emit fired');
});
me.doStuff();
// Ausgabe:
// vorher
// emitted fired
// danach
Hinweis* Wenn emitter.emit asynchron ist, sollte die Ausgabe
sein
// vorher
// danach
// emitted fired
Mechanismusübersicht und Thread-Pool
Der Knoten selbst ist auf mehrere Bibliotheken angewiesen. Eine davon ist libuv, die erstaunliche Bibliothek für die Handhabung asynchroner Ereigniswarteschlangen und deren Ausführung.
Knoten nutzt so viel wie möglich vom Betriebssystemkernel, um vorhandene Funktionen zu implementieren. Zum Beispiel Antwortanfragen generieren, Verbindungen weiterleiten und diese dem System zur Verarbeitung anvertrauen. Beispielsweise werden eingehende Verbindungen über das Betriebssystem in die Warteschlange gestellt, bis sie von Node verarbeitet werden können.
Sie haben vielleicht gehört, dass Node über einen Thread-Pool verfügt, und fragen sich vielleicht: „Wenn Node Aufgaben der Reihe nach verarbeitet, warum brauchen wir dann einen Thread-Pool?“ Dies liegt daran, dass im Kernel nicht alle Aufgaben verarbeitet werden Auftrag. Wird asynchron ausgeführt. In diesem Fall muss Node.JS in der Lage sein, den Thread während des Betriebs für eine bestimmte Zeitspanne zu sperren, damit er die Ereignisschleife weiterhin ausführen kann, ohne blockiert zu werden.
Das Folgende ist ein einfaches Beispieldiagramm, das den internen Betriebsmechanismus zeigt:
┌──────────────────────┐
╭──►│ Timer Timer
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
│ Ausstehende Rückrufe
│ └──────────┬────────────┘
|
│ │ │ UMFRAGE ││── Verbindungen, │
│ seitdem
│ ┌───────────┴───────────┐
╰─── ┤ setImmediate
└───────────────────────┘
Es gibt einige Dinge, die an der internen Funktionsweise der Ereignisschleife schwer zu verstehen sind:
Alle Rückrufe werden über process.nextTick() am Ende einer Phase der Ereignisschleife (z. B. Timer) und vor dem Übergang zur nächsten Phase voreingestellt. Dadurch werden potenzielle rekursive Aufrufe von process.nextTick() vermieden, die eine Endlosschleife verursachen.
„Ausstehende Rückrufe“ sind Rückrufe in der Rückrufwarteschlange, die von keinem anderen Ereignisschleifenzyklus verarbeitet werden (z. B. an fs.write übergeben).
Vereinfachen Sie die Interaktion mit der Ereignisschleife, indem Sie einen EventEmitter erstellen. Es handelt sich um einen generischen Wrapper, mit dem Sie ereignisbasierte APIs einfacher erstellen können. Wie die beiden interagieren, verwirrt Entwickler oft.
Das folgende Beispiel zeigt, dass das Vergessen, dass ein Ereignis synchron ausgelöst wird, dazu führen kann, dass das Ereignis verpasst wird.
Funktion MyThing() {
EventEmitter.call(this);
doFirstThing();
this.emit('thing1');
}
util.inherits(MyThing, EventEmitter);
var mt = new MyThing();
mt.on('thing1', function onThing1() {
// Entschuldigung, dieses Ereignis wird nie stattfinden
});
Funktion MyThing() {
EventEmitter.call(this);
doFirstThing();
setImmediate(emitThing1, this);
}
util.inherits(MyThing, EventEmitter);
Funktion emitThing1(self) {
self.emit('thing1');
}
var mt = new MyThing();
mt.on('thing1', function onThing1() {
// Ausgeführt
});
Die folgende Lösung funktioniert auch, allerdings auf Kosten der Leistung:
doFirstThing();
// Die Verwendung von Function#bind() führt zu Leistungseinbußen
setImmediate(this.emit.bind(this, 'thing1'));
}
util.inherits(MyThing, EventEmitter);
// Fehler auslösen und sofort (synchron) behandeln
var er = doSecondThing();
if (er) {
This.emit('error', 'More bad stuff');
Zurück;
}
}
Fazit
In diesem Artikel werden kurz das Innenleben und die technischen Details der Ereignisschleife erläutert. Es ist alles gut durchdacht. Ein weiterer Artikel befasst sich mit der Interaktion der Ereignisschleife mit dem Systemkernel und zeigt die Magie des asynchronen NodeJS-Betriebs.