Der Inhalt dieses Artikels befasst sich mit der Analyse des Ereignisschleifenmechanismus von Node.js. Ich hoffe, dass er für Freunde hilfreich ist.
Im Browser-Artikel wurden der Ereignisschleifenmechanismus und einige verwandte Konzepte ausführlich vorgestellt, dies dient jedoch hauptsächlich der Forschung auf der Browserseite. Gilt das auch für die Node-Umgebung? Schauen wir uns zunächst eine Demo an:
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') })}, 0)setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') })}, 0)
Kompilieren Sie es mit bloßem Auge und führen Sie es im Browser aus. Sie kennen die Wahrheit bereits, daher werde ich nicht auf Details eingehen.
timer1 promise1 timer2 promise2
Dann führe es unter Node aus, eh. . . Seltsam, das laufende Ergebnis unterscheidet sich vom Browser~
timer1 timer2 promise1 promise2
Das Beispiel zeigt, dass der Ereignisschleifenmechanismus des Browsers und von Node.js unterschiedlich sind, werfen wir einen Blick darauf~
Ereignisverarbeitung von Node.js
Node.js verwendet V8 als Parsing-Engine von js und verwendet eine eigene libuv für die E/A-Verarbeitung. libuv ist eine ereignisgesteuerte plattformübergreifende Abstraktionsschicht, die einige zugrunde liegende Funktionen von kapselt Verschiedene Betriebssysteme stellen eine einheitliche API für die Außenwelt bereit. Der Ereignisschleifenmechanismus ist darin ebenfalls implementiert:
int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; r = uv__loop_alive(loop); if (!r) uv__update_time(loop); while (r != 0 && loop->stop_flag == 0) { uv__update_time(loop); // timers阶段 uv__run_timers(loop); // I/O callbacks阶段 ran_pending = uv__run_pending(loop); // idle阶段 uv__run_idle(loop); // prepare阶段 uv__run_prepare(loop); timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop); // poll阶段 uv__io_poll(loop, timeout); // check阶段 uv__run_check(loop); // close callbacks阶段 uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { uv__update_time(loop); uv__run_timers(loop); } r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; } if (loop->stop_flag != 0) loop->stop_flag = 0; return r; }
Laut der offiziellen Einführung von Node.js enthält jede Ereignisschleife 6 Stufen . , entsprechend der Implementierung im libuv-Quellcode, wie in der Abbildung unten gezeigt
Timer-Stufe: Diese Stufe führt den Rückruf von Timer (setTimeout, setInterval) aus
E/A-Rückrufphase: Führen Sie einige Systemaufruffehler aus, z. B. Netzwerkkommunikationsfehler-Rückrufe
Leerlauf, Vorbereitungsphase: Wird nur intern vom Knoten verwendet
Abfragephase: Neues I abrufen /O-Ereignisse, entsprechend Unter den Bedingungen wird der Knoten hier blockiert
Prüfphase: Führen Sie den Rückruf von setImmediate() aus
Rückrufphase schließen: Führen Sie den Rückruf des Schließereignisses des Sockets aus
Konzentrieren wir uns auf Timer und Umfragen. Überprüfen Sie einfach diese drei Phasen, da die meisten asynchronen Aufgaben in der täglichen Entwicklung in diesen drei Phasen verarbeitet werden.
Timer-Phase
Timer ist die erste Phase der Ereignisschleife, Node Überprüft, ob ein Timer abgelaufen ist, und wenn ja, wird der Rückruf in die Taskwarteschlange des Timers verschoben, um auf die Ausführung zu warten Es gibt keine Garantie dafür, dass der Timer sofort ausgeführt wird, wenn die voreingestellte Zeit erreicht ist, da die Ablaufprüfung des Timers durch den Knoten nicht unbedingt zuverlässig ist. Dies wird von anderen laufenden Programmen auf dem Computer beeinflusst oder der Hauptthread ist nicht inaktiv damals. Im folgenden Code ist beispielsweise die Ausführungsreihenfolge von setTimeout() und setImmediate() ungewiss.
setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') })
Wenn Sie sie jedoch in einen I/O-Rückruf einfügen, muss setImmediate() zuerst ausgeführt werden, da auf die Abfragephase die Prüfphase folgt.
Umfragephase
Die Umfragephase hat hauptsächlich zwei Funktionen:
Verarbeitung von Ereignissen in der Umfragewarteschlange
Wenn ein Timer abgelaufen ist, Führen Sie seine Rückruffunktion aus.
Die gerade Schleife führt die Rückrufe in der Abfragewarteschlange synchron aus, bis die Warteschlange leer ist oder die ausgeführten Rückrufe die Obergrenze des Systems erreichen (die Obergrenze ist unbekannt). Anschließend prüft die gerade Schleife, ob Es gibt ein voreingestelltes setImmediate(), das in zwei Situationen unterteilt ist:
Wenn es ein voreingestelltes setImmediate() gibt, beendet die Ereignisschleife die Abfragephase, tritt in die Prüfphase ein und führt die Aufgabenwarteschlange der Prüfung aus Phase
Wenn kein voreingestelltes setImmediate() vorhanden ist, blockiert die Ereignisschleife und wartet in dieser Phase
Beachten Sie ein Detail: Kein setImmediate() löst ein Ereignis aus Die Schleife ist in der Poll-Phase blockiert, könnte also der zuvor eingestellte Timer nicht nicht ausgeführt werden? Also, in der Umfragephase Die Schleife verfügt über einen Prüfmechanismus, um zu prüfen, ob die Timer-Warteschlange leer ist. Wenn die Timer-Warteschlange nicht leer ist, wird das Ereignis ausgeführt Die Schleife startet die nächste Runde der Ereignisschleife, d. h. sie tritt erneut in die Timer-Phase ein.
Prüfphase
Der Rückruf von setImmediate() wird zur Prüfwarteschlange hinzugefügt. Aus dem Phasendiagramm der Ereignisschleife können wir erkennen, dass die Ausführungsreihenfolge der Prüfphase nach der Abfrage liegt Phase.
Zusammenfassung
Jede Stufe der Ereignisschleife hat eine Aufgabenwarteschlange
Wenn die Ereignisschleife eine bestimmte Stufe erreicht, wird die Aufgabenwarteschlange dieser Stufe bis zur Warteschlange ausgeführt wird gelöscht. Oder wenn der ausgeführte Rückruf das Systemlimit erreicht, geht er zur nächsten Stufe über
Wenn alle Stufen einmal nacheinander ausgeführt werden, gilt die Ereignisschleife als abgeschlossenes Tick
Das macht Sinn. Aber ohne die Demo verstehe ich es immer noch nicht ganz.
const fs = require('fs')fs.readFile('test.txt', () => { console.log('readFile') setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) })
Es sollte kein Zweifel an den Ausführungsergebnissen bestehen
readFile immediate timeout
Unterschiede zwischen Node.js und der Browser-Ereignisschleife
Überprüfung des vorherigen Artikels, Mikrotask-Aufgaben in der Browserumgebung Die Warteschlange wird nach der Ausführung jeder Makroaufgabe ausgeführt.
In Node.js werden Mikrotasks zwischen verschiedenen Phasen der Ereignisschleife ausgeführt, d. h. nach Ausführung einer Phase werden die Aufgaben in der Mikrotask-Warteschlange ausgeführt .
Demo-Rezension
Bei der Durchsicht der Demo am Anfang des Artikels wird das globale Skript (main()) ausgeführt. und die beiden Timer werden nacheinander in die Timer-Warteschlange gestellt, main() wird ausgeführt, der Aufrufstapel ist inaktiv und die Task-Warteschlange beginnt mit der Ausführung
首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;
至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2。
对比浏览器端的处理过程:
process.nextTick() VS setImmediate()
In essence, the names should be swapped. process.nextTick() fires more immediately than setImmediate()
来自官方文档有意思的一句话,从语义角度看,setImmediate() 应该比 process.nextTick() 先执行才对,而事实相反,命名是历史原因也很难再变。
process.nextTick() 会在各个事件阶段之间执行,一旦执行,要直到nextTick队列被清空,才会进入到下一个事件阶段,所以如果递归调用 process.nextTick(),会导致出现I/O starving(饥饿)的问题,比如下面例子的readFile已经完成,但它的回调一直无法执行:
const fs = require('fs')const starttime = Date.now()let endtime fs.readFile('text.txt', () => { endtime = Date.now() console.log('finish reading time: ', endtime - starttime)})let index = 0function handler () { if (index++ >= 1000) return console.log(`nextTick ${index}`) process.nextTick(handler) // console.log(`setImmediate ${index}`) // setImmediate(handler)}handler()
process.nextTick()的运行结果:
nextTick 1 nextTick 2 ...... nextTick 999 nextTick 1000 finish reading time: 170
替换成setImmediate(),运行结果:
setImmediate 1 setImmediate 2 finish reading time: 80 ...... setImmediate 999 setImmediate 1000
这是因为嵌套调用的 setImmediate() 回调,被排到了下一次event loop才执行,所以不会出现阻塞。
总结
1、Node.js 的事件循环分为6个阶段
2、浏览器和Node 环境下,microtask 任务队列的执行时机不同
Node.js中,microtask 在事件循环的各个阶段之间执行
浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
3、递归的调用process.nextTick()会导致I/O starving,官方推荐使用setImmediate()
Das obige ist der detaillierte Inhalt vonAnalyse des Ereignisschleifenmechanismus von Node.js. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!