So starten Sie schnell mit VUE3.0: Einstieg ins Lernen
Wenn es um Node.js geht, denke ich, dass die meisten Front-End-Ingenieure daran denken werden, darauf basierende Server zu entwickeln. Sie müssen nur JavaScript beherrschen, um ein Full-Stack zu werden Ingenieur, aber tatsächlich ist die Bedeutung von Node.js Und das ist noch nicht alles.
Bei vielen Hochsprachen können Ausführungsberechtigungen das Betriebssystem erreichen, aber JavaScript, das auf der Browserseite ausgeführt wird, ist eine Ausnahme. Die vom Browser erstellte Sandbox-Umgebung versiegelt Front-End-Ingenieure in einem Elfenbeinturm in der Programmierwelt. Das Aufkommen von Node.js hat dieses Manko jedoch wettgemacht, und Front-End-Ingenieure können auch bis in die Tiefen der Computerwelt vordringen.
Daher besteht die Bedeutung von Nodejs für Front-End-Ingenieure nicht nur darin, Full-Stack-Entwicklungsfunktionen bereitzustellen, sondern, was noch wichtiger ist, darin, Front-End-Ingenieuren eine Tür zur zugrunde liegenden Welt der Computer zu öffnen. Dieser Artikel öffnet diese Tür, indem er die Implementierungsprinzipien von Node.js analysiert.
Im Verzeichnis /deps des Node.js-Quellcode-Warehouses gibt es mehr als ein Dutzend Abhängigkeiten, darunter in C-Sprache geschriebene Module (z. B. libuv, V8) und in JavaScript-Sprache geschriebene Module (z. B. acorn). , Acorn-Plugins).
Die wichtigsten davon sind die Module, die den Verzeichnissen v8 und uv entsprechen. V8 selbst kann nicht asynchron ausgeführt werden, sondern wird mithilfe anderer Threads im Browser implementiert. Aus diesem Grund sagen wir oft, dass js Single-Threaded ist, da seine Parsing-Engine nur synchronen Parsing-Code unterstützt. Aber in Node.js basiert die asynchrone Implementierung hauptsächlich auf libuv. Konzentrieren wir uns auf die Analyse des Implementierungsprinzips von libuv.
libuv ist eine in C geschriebene asynchrone E/A-Bibliothek, die mehrere Plattformen unterstützt. Sie löst hauptsächlich das Problem von E/A-Vorgängen, die leicht zu Blockierungen führen. Es wurde ursprünglich speziell für die Verwendung mit Node.js entwickelt, wird aber seitdem von anderen Modulen wie Luvit, Julia und Pyuv verwendet. Die folgende Abbildung ist das Strukturdiagramm von libuv.
libuv verfügt über zwei asynchrone Implementierungsmethoden. Dies sind die beiden Teile, die durch das gelbe Kästchen links und rechts im Bild oben ausgewählt werden.
Der linke Teil ist das Netzwerk-E/A-Modul, das auf verschiedenen Plattformen über unterschiedliche Implementierungsmechanismen verfügt. Linux-Systeme verwenden Epoll, um es zu implementieren, OSX- und andere BSD-Systeme verwenden KQueue, SunOS-Systeme verwenden Ereignisports und Windows-Systeme verwenden IOCP. Da es sich um die zugrunde liegende API des Betriebssystems handelt, ist es komplizierter zu verstehen, daher werde ich es hier nicht vorstellen.
Der rechte Teil umfasst das Datei-E/A-Modul, das DNS-Modul und den Benutzercode, der asynchrone Vorgänge über den Thread-Pool implementiert. Datei-E/A unterscheidet sich von Netzwerk-E/A. libuv verlässt sich nicht auf die zugrunde liegende API des Systems, sondern führt blockierte Datei-E/A-Vorgänge im globalen Thread-Pool aus.
Die folgende Abbildung ist das Workflowdiagramm für die Ereignisabfrage auf der offiziellen Website von libuv. Lassen Sie es uns zusammen mit dem Code analysieren.
Der Kerncode der libuv-Ereignisschleife ist in der Funktion uv_run() implementiert. Das Folgende ist Teil des Kerncodes unter dem Unix-System. Obwohl es in der Sprache C geschrieben ist, handelt es sich um eine Hochsprache wie JavaScript, sodass es nicht allzu schwer zu verstehen ist. Der größte Unterschied dürften die Sternchen und Pfeile sein. Wir können die Sternchen einfach ignorieren. Beispielsweise kann die Schleife uv_loop_t* im Funktionsparameter als variable Schleife vom Typ uv_loop_t verstanden werden. Der Pfeil „→“ kann als Punkt „.“ verstanden werden, zum Beispiel kann loop→stop_flag als loop.stop_flag verstanden werden.
int uv_run(uv_loop_t* loop, uv_run_mode mode) { ... r = uv__loop_alive(loop); if (!r) uv__update_time(loop); while (r != 0 && Schleife - >stop_flag == 0) { uv__update_time(loop); uv__run_timers(loop); ran_pending = uv__run_pending(loop); uv__run_idle(loop); uv__run_prepare(loop);...uv__io_poll(loop, timeout); uv__run_check(loop); uv__run_closing_handles(loop);... }... }
uv__loop_alive
Diese Funktion wird verwendet, um zu bestimmen, ob die Ereignisabfrage fortgesetzt werden soll. Wenn im Schleifenobjekt keine aktive Aufgabe vorhanden ist, wird 0 zurückgegeben und die Schleife verlassen.
In der C-Sprache hat diese „Aufgabe“ einen professionellen Namen, nämlich „Handle“, der als Variable verstanden werden kann, die auf die Aufgabe verweist. Handles können in zwei Kategorien unterteilt werden: Request und Handle, die Handles mit kurzem Lebenszyklus bzw. Handles mit langem Lebenszyklus darstellen. Der spezifische Code lautet wie folgt:
static int uv__loop_alive(const uv_loop_t * loop) { return uv__has_active_handles(loop) ||. uv__has_active_reqs(loop) ||. }
uv__update_time
Um die Anzahl zeitbezogener Systemaufrufe zu reduzieren, wird diese Funktion zum Zwischenspeichern der aktuellen Systemzeit verwendet. Die Genauigkeit ist sehr hoch und kann den Nanosekundenbereich erreichen, die Einheit beträgt jedoch immer noch Millisekunden.
Der spezifische Quellcode lautet wie folgt:
UV_UNUSED(static void uv__update_time(uv_loop_t * loop)) { Schleife - >time = uv__hrtime(UV_CLOCK_FAST) / 1000000; }
uv__run_timers
führt die Rückruffunktionen aus, die den Zeitschwellenwert in setTimeout() und setInterval() erreichen. Dieser Ausführungsprozess wird durch For-Schleifendurchlauf implementiert. Wie Sie dem folgenden Code entnehmen können, wird der Timer-Rückruf in den Daten einer minimalen Heap-Struktur gespeichert. Er wird beendet, wenn der minimale Heap-Speicher leer ist oder den Zeitschwellenwert nicht erreicht hat .
Entfernen Sie den Timer, bevor Sie die Timer-Rückruffunktion ausführen. Wenn die Wiederholung festgelegt ist, muss sie erneut zum minimalen Heap hinzugefügt werden, und dann wird der Timer-Rückruf ausgeführt.
Der spezifische Code lautet wie folgt:
void uv__run_timers(uv_loop_t * loop) { struct heap_node * heap_node; uv_timer_t * handle; für (;;) { heap_node = heap_min(timer_heap(loop)); if (heap_node == NULL) break; handle = container_of(heap_node, uv_timer_t, heap_node); if (handle - >timeout > loop - >time) break; uv_timer_stop(handle); uv_timer_again(handle); handle ->timer_cb(handle); } }
uv__run_pending
durchläuft alle in pending_queue gespeicherten E/A-Callback-Funktionen und gibt 0 zurück, wenn pending_queue leer ist. Andernfalls wird 1 zurückgegeben, nachdem die Callback-Funktion in pending_queue ausgeführt wurde.
Der Code lautet wie folgt:
static int uv__run_pending(uv_loop_t * loop) { WARTESCHLANGE * q; Warteschlange pq; uv__io_t * w; if (QUEUE_EMPTY( & Schleife - >pending_queue)) return 0; QUEUE_MOVE( & Schleife - >pending_queue, &pq); while (!QUEUE_EMPTY( & pq)) { q = QUEUE_HEAD( & pq); QUEUE_REMOVE(q); QUEUE_INIT(q); w = QUEUE_DATA(q, uv__io_t, pending_queue); w - >cb(loop, w, POLLOUT); } Rückgabe 1; }Die drei Funktionen
uvrun_idle / uvrun_prepare / uv__run_check
werden alle über eine Makrofunktion UV_LOOP_WATCHER_DEFINE definiert. Die Makrofunktion kann als Codevorlage oder als Funktion zum Definieren von Funktionen verstanden werden. Die Makrofunktion wird dreimal aufgerufen und die Namensparameterwerte „prepare“, „check“ und „idle“ werden jeweils übergeben. Gleichzeitig werden drei Funktionen definiert: uvrun_idle, uvrun_prepare und uv__run_check.
Daher ist ihre Ausführungslogik konsistent. Sie durchlaufen alle Objekte in der Warteschlangenschleife->name##_handles nach dem First-In-First-Out-Prinzip und führen dann die entsprechende Rückruffunktion aus.
#define UV_LOOP_WATCHER_DEFINE(Name, Typ) void uv__run_##name(uv_loop_t* loop) { uv_##name##_t* h; QUEUE-Warteschlange; WARTESCHLANGE* q; QUEUE_MOVE(&loop->name##_handles, &queue); while (!QUEUE_EMPTY(&queue)) { q = QUEUE_HEAD(&queue); h = QUEUE_DATA(q, uv_##name##_t, queue); QUEUE_REMOVE(q); QUEUE_INSERT_TAIL(&loop->name##_handles, q); h->name##_cb(h); } } UV_LOOP_WATCHER_DEFINE(vorbereiten, VORBEREITEN) UV_LOOP_WATCHER_DEFINE(prüfen, prüfen) UV_LOOP_WATCHER_DEFINE(idle, IDLE)
uv__io_poll
uv__io_poll wird hauptsächlich zum Abfragen von E/A-Vorgängen verwendet. Die konkrete Implementierung variiert je nach Betriebssystem. Wir nehmen das Linux-System als Beispiel für die Analyse.
Die Funktion uv__io_poll enthält eine Menge Quellcode. Der Kern besteht aus zwei Schleifencodes. Ein Teil des Codes lautet wie folgt:
void uv__io_poll(uv_loop_t * loop, int timeout) { while (!QUEUE_EMPTY( & Schleife - >watcher_queue)) { q = QUEUE_HEAD( & loop - >watcher_queue); QUEUE_REMOVE(q); QUEUE_INIT(q); w = QUEUE_DATA(q, uv__io_t, watcher_queue); e.events = w ->pevents; e.data.fd = w ->fd; if (w - >events == 0) op = EPOLL_CTL_ADD; sonst op = EPOLL_CTL_MOD; if (epoll_ctl(loop - >backend_fd, op, w - >fd, &e)) { if (errno != EEXIST) abort(); if (epoll_ctl(loop - >backend_fd, EPOLL_CTL_MOD, w - >fd, &e)) abort(); } w - >events = w - >pevents; } für (;;) { für (i = 0; i < nfds; i++) { pe = Ereignisse + i; fd = pe ->data.fd; w = loop - >watchers[fd]; pe - >events &= w - >pevents |. if (pe - >events == POLLERR || pe - >events == POLLHUP) pe - >events |= w - >pevents & (POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI); if (pe - >events != 0) { if (w == &loop - >signal_io_watcher) have_signals = 1; else w - >cb(loop, w, pe - >events); Ereignisse++; } } if (have_signals != 0) loop - >signal_io_watcher.cb(loop, &loop - >signal_io_watcher, POLLIN); }... }
Durchlaufen Sie in der while-Schleife die Beobachterwarteschlange watcher_queue, nehmen Sie den Ereignis- und Dateideskriptor heraus, weisen Sie sie dem Ereignisobjekt e zu und rufen Sie dann die Funktion epoll_ctl auf, um das Epoll-Ereignis zu registrieren oder zu ändern.
In der for-Schleife wird der in epoll wartende Dateideskriptor herausgenommen und nfds zugewiesen. Anschließend wird nfds durchlaufen, um die Rückruffunktion auszuführen.
uv__run_closing_handles
durchläuft die Warteschlange, die darauf wartet, geschlossen zu werden, schließt Handles wie Stream, TCP, UDP usw. und ruft dann die dem Handle entsprechende close_cb auf. Der Code lautet wie folgt:
static void uv__run_closing_handles(uv_loop_t * loop) { uv_handle_t * p; uv_handle_t * q; p = Schleife ->closing_handles; Schleife ->closing_handles = NULL; während (p) { q = p ->next_closing; uv__finish_close(p); p = q; } }
Obwohl Process.nextTick und Promise beide asynchrone APIs sind, sind sie nicht Teil der Ereignisabfrage. Sie verfügen über eigene Aufgabenwarteschlangen, die nach Abschluss jedes Schritts der Ereignisabfrage ausgeführt werden. Wenn wir diese beiden asynchronen APIs verwenden, müssen wir darauf achten, dass die Ereignisabfrage blockiert wird, wenn lange Aufgaben oder Rekursionen in der eingehenden Rückruffunktion ausgeführt werden, wodurch E/A-Vorgänge „ausgehungert“ werden.
Der folgende Code ist ein Beispiel, bei dem die Rückruffunktion von fs.readFile nicht durch rekursiven Aufruf von prcoess.nextTick ausgeführt werden kann.
fs.readFile('config.json', (err, data) = >{... }) const traverse = () = >{ process.nextTick(traverse) }
Um dieses Problem zu lösen, können Sie stattdessen setImmediate verwenden, da setImmediate die Rückruffunktionswarteschlange in der Ereignisschleife ausführt. Die Aufgabenwarteschlange „process.nextTick“ hat eine höhere Priorität als die Aufgabenwarteschlange „Promise“. Die spezifischen Gründe finden Sie im folgenden Code:
function ProcessTicksAndRejections() { tocken lassen; Tun { while (tock = queue.shift()) { const asyncId = tock[async_id_symbol]; emitBefore(asyncId, tock[trigger_async_id_symbol], tock); versuchen { const callback = tock.callback; if (tock.args === undefiniert) { Rückruf(); } anders { const args = tock.args; switch (arg. Länge) { Fall 1: Rückruf(args[0]); brechen; Fall 2: Rückruf(args[0], args[1]); brechen; Fall 3: Rückruf(args[0], args[1], args[2]); brechen; Fall 4: Rückruf(args[0], args[1], args[2], args[3]); brechen; Standard: Rückruf(...Argumente); } } } Endlich { if (destroyHooksExist()) emitDestroy(asyncId); } emitAfter(asyncId); } runMicrotasks(); } while (! queue . isEmpty () || ProcessPromiseRejections()); setHasTickScheduled(false); setHasRejectionToWarn(false); }
Wie aus der Funktion ProcessTicksAndRejections () hervorgeht, wird die Rückruffunktion der Warteschlangenwarteschlange zunächst durch die While-Schleife herausgenommen und die Rückruffunktion in der Warteschlangenwarteschlange über Process.nextTick hinzugefügt. Wenn die while-Schleife endet, wird die Funktion runMicrotasks() aufgerufen, um die Callback-Funktion Promise auszuführen.
die Kernstruktur von Node.js, die auf libuv basiert, in zwei Teile unterteilt werden kann. Ein Teil ist Netzwerk-E/A. Der andere Teil ist Datei I /O, DNS und Benutzercode. Verwenden Sie den Thread-Pool für die Verarbeitung.
Der Kernmechanismus von libuv zur Verarbeitung asynchroner Vorgänge ist die Ereignisabfrage. Die allgemeine Operation besteht darin, die Rückruffunktion in der Warteschlange zu durchlaufen.
Abschließend wird erwähnt, dass die asynchronen APIs „process.nextTick“ und „Promise“ nicht zur Ereignisabfrage gehören. Eine unsachgemäße Verwendung führt dazu, dass die Ereignisabfrage blockiert wird. Eine Lösung besteht darin, stattdessen setImmediate zu verwenden.