Heim  >  Artikel  >  Web-Frontend  >  JavaScript-Ausführungsmechanismus und Ereignisschleife in der NodeJS-Umgebung

JavaScript-Ausführungsmechanismus und Ereignisschleife in der NodeJS-Umgebung

不言
不言nach vorne
2019-04-02 10:59:592525Durchsuche

Dieser Artikel bietet Ihnen eine Einführung in die Verwendung von JavaScript-Funktionen höherer Ordnung. Ich hoffe, dass er Ihnen als Referenz dienen wird.

1. Beschreibung

nodejs wird in einem einzelnen Thread ausgeführt und basiert auf einem ereignisgesteuerten, nicht blockierenden IO-Programmiermodell. Dadurch können wir den Code weiter ausführen, ohne auf die Rückkehr des Ergebnisses des asynchronen Vorgangs warten zu müssen. Wenn ein asynchrones Ereignis ausgelöst wird, wird der Hauptthread benachrichtigt und der Hauptthread führt den Rückruf des entsprechenden Ereignisses aus.

In diesem Artikel wird der Ausführungsprozess von JavaScript-Code im Knoten erläutert. Wenn Sie das Ausgabeergebnis kennen, müssen Sie diesen Artikel nicht lesen Ausgabeergebnis, dann kann Ihnen dieses Video helfen, Folgendes zu verstehen:

console.log(1)
setTimeout(function () {
  new Promise(function (resolve) {
    console.log(2)
    resolve()
  })
  .then(() => { console.log(3) })
})
setTimeout(function () {
  console.log(4)
})

Komplex:

setTimeout(() => {
  console.log('1')
  new Promise((resolve) => { console.log('2'); resolve(); })
  .then(() => { console.log('3') })
  new Promise((resolve)=> { console.log('4'); resolve()})
  .then(() => { console.log('5') })
  setTimeout(() => { 
    console.log('6')
    setTimeout(() => {
      console.log('7')
      new Promise((resolve) => { console.log('8'); resolve() })
      .then( () => {  console.log('9') })
      new Promise((resolve) => { console.log('10'); resolve() })
      .then(() => {  console.log('11') })
    })
    setTimeout(() => { console.log('12') }, 0)
  })
  setTimeout(() => { console.log('13') }, 0)
})
setTimeout(() => { console.log('14') }, 0)
new Promise((resolve) => { console.log('15'); resolve() })
.then( ()=> { console.log('16') })
new Promise((resolve) => { console.log('17'); resolve() })
.then(() => { console.log('18') })

2. Der Startvorgang von nodejs

Der Knoten Der .js-Startvorgang kann in die folgenden Schritte unterteilt werden:

  1. Rufen Sie die platformInit-Methode auf, um die NodeJS-Betriebsumgebung zu initialisieren.
  2. Rufen Sie die Methode performance_node_start auf, um Leistungsstatistiken für NodeJS durchzuführen.
  3. Beurteilung der OpenSSL-Einstellungen.
  4. Rufen Sie v8_platform.Initialize auf, um den libuv-Thread-Pool zu initialisieren.
  5. Rufen Sie V8::Initialize auf, um die V8-Umgebung zu initialisieren.
  6. Erstellen Sie eine NodeJS-Laufinstanz.
  7. Starten Sie die im vorherigen Schritt erstellte Instanz.
  8. Starten Sie die Ausführung der js-Datei. Nachdem der Synchronisierungscode ausgeführt wurde, treten Sie in die Ereignisschleife ein.
  9. Wenn keine Ereignisse zum Abhören vorhanden sind, wird die NodeJS-Instanz zerstört und die Programmausführung ist abgeschlossen.

JavaScript-Ausführungsmechanismus und Ereignisschleife in der NodeJS-Umgebung

3. Detaillierte Erklärung der Nodejs-Ereignisschleife

Nodejs unterteilt die Nachrichtenschleife in 6 Phasen (offiziell Phase genannt). ), jede Stufe verfügt über eine warteschlangenartige Struktur, in der die Rückruffunktionen gespeichert werden, die in dieser Stufe verarbeitet werden müssen.

Nodejs soll verhindern, dass zu viele Aufgaben in einer bestimmten Stufe zu einem Mangel in nachfolgenden Stufen führen. Daher wird in jeder Iteration der Nachrichtenschleife eine maximale Anzahl von Rückrufen in jeder Phase ausgeführt. Wenn die Anzahl überschritten wird, wird die aktuelle Phase zwangsweise beendet und in die nächste Phase eingetreten die Nachrichtenschleife. Eine Stufe.

3.1 Timer-Stufe

Dies ist die erste Stufe der Nachrichtenschleife, bei der eine for-Schleife verwendet wird, um alle setTimeout- und setInterval-Rückrufe zu verarbeiten.

Diese Rückrufe werden in einem minimalen Heap (Min-Heap) gespeichert. Auf diese Weise muss die Engine jedes Mal nur das Header-Element beurteilen. Wenn es die Bedingungen erfüllt, wird es nicht ausgeführt Beenden Sie die Timer-Phase, bis ein Element gefunden wird, das die Bedingungen nicht erfüllt, oder die Warteschlange leer ist.

Die Methode zur Bestimmung, ob ein bestimmter Rückruf die Bedingungen in der Timer-Phase erfüllt, ist ebenfalls sehr einfach Wenn die Nachrichtenschleife in den Timer eintritt, wird die Systemzeit zu diesem Zeitpunkt gespeichert. Schauen Sie sich dann einfach die Einstellungen der Rückruffunktion im obigen Mindesthaufen an. Ob die Startzeit die beim Betreten des Timers gespeicherte Zeit überschreitet. Wenn sie überschritten wird, nehmen Sie sie aus und führen Sie es aus.

3.2 Ausstehende E/A-Rückrufphase

führt aus, außer close callbacks, setTimeout() und setInterval(), fast alle Rückrufe außer setImmediate() Rückrufe, wie z TCP连接发生错误, fs.read, socket und andere Rückruffunktionen für IO-Operationen sowie verschiedene Fehlerrückrufe

3.3 Leerlauf, Vorbereitungsphase

Einige Aufrufe innerhalb des Systems.

3.4 Abfragephase, wichtige Phase

Dies ist die wichtigste Phase im gesamten Nachrichtenzyklus. Ihre Aufgabe besteht darin, auf asynchrone Anforderungen und Daten zu warten, da sie den gesamten Nachrichtenzyklusmechanismus unterstützt.

Die Umfragephase hat zwei Hauptfunktionen: Eine besteht darin, den Rückruf von Timern auszuführen, deren untere Grenzzeit erreicht wurde, und die andere darin, Ereignisse in der Umfragewarteschlange zu verarbeiten.
Hinweis: Viele APIs von Node basieren auf einem Ereignisabonnement, wie z. B. fs.readFile. Diese Rückrufe sollten in der Phase poll abgeschlossen werden.

Wenn die Ereignisschleife in die Abfragephase eintritt:

  • pollWenn die Warteschlange nicht leer ist, muss die Ereignisschleife zuerst die Warteschlange durchlaufen und den Rückruf synchron ausführen, bis die Warteschlange erreicht ist gelöscht oder der Rückruf ausgeführt wird. Die Anzahl erreicht das Systemlimit.
  • pollWenn die Warteschlange leer ist, gibt es zwei Situationen.

    • Wenn der Code mit einem Rückruf von setImmediate() festgelegt wurde, beendet die Ereignisschleife direkt die poll-Phase und tritt in die check-Phase ein, um den Rückruf im check auszuführen Warteschlange.
    • Wenn der Code nicht festgelegt wurde setImmediate() Rückruf festlegen:

      • Wenn Timer eingestellt sind, überprüft die Ereignisschleife zu diesem Zeitpunkt die Timer. Wenn einer oder mehrere Timer das untere Limit erreicht haben, kehrt die Ereignisschleife zur Timer-Phase zurück und führt die effektive Aktion aus Rückruf der Timer-Warteschlange.
      • Wenn keine Timer eingestellt sind, blockiert die Ereignisschleife zu diesem Zeitpunkt und wartet darauf, dass der Ereignisrückruf in der Umfragephase zur Umfragewarteschlange hinzugefügt wird.

Abfragephase: Wenn die vom js-Layer-Code registrierten Ereignisrückrufe nicht zurückkehren, wird die Ereignisschleife in der Abfragephase vorübergehend blockiert und entsperrte Bedingungen:

  1. Wenn die Poll-Phase ausgeführt wird, wird ein Timeout-Timeout übergeben, das die maximale Blockierungszeit der Poll-Phase darstellt.
  2. Wenn die Timeout-Zeit noch nicht abgelaufen ist und ein Ereignis zurückkehrt, wird die für das Ereignis registrierte Rückruffunktion ausgeführt. Wenn das Timeout-Timeout abläuft, wird die Umfragephase beendet und die nächste Phase wird ausgeführt.

Was ist die geeignete Einstellung für dieses Timeout? Die Antwort ist der Unterschied zwischen der Startzeit des kürzlich in der Timer-Phase auszuführenden Rückrufs und dem aktuellen Zeitpunkt, vorausgesetzt, dass der Unterschied da ist wartet nichts darauf, nach dem Poll-Phase-Callback ausgeführt zu werden. Wenn ein Ereignis die Nachrichtenschleife während des Zeitraums aufweckt, wird die Arbeit der nächsten Phase fortgesetzt Nach dem Timeout muss die Nachrichtenschleife noch in die nachfolgende Phase eintreten, sodass auch die Timer-Phase der nächsten Iteration ausgeführt werden kann.
Nodejs führt die gesamte Nachrichtenschleife durch die Poll-Phase und wartet auf E/A-Ereignisse und das Eintreffen des Kernels asynchrone Ereignisse.

3.5 Prüfphase

Diese Phase behandelt nur die Rückruffunktion von setImmediate.
Warum gibt es also eine spezielle Phase, um setImmediate zu verarbeiten? Da in der Umfragephase möglicherweise einige Rückrufe festgelegt werden, wird diese Prüfphase nach der Umfragephase hinzugefügt.

ist speziell für die Bearbeitung einiger Rückrufe konzipiert Rückrufe vom Typ schließen. Zum Beispiel:

Es wird für die Ressourcenbereinigung verwendet

socket.on('close', ...)4 >Knotenumgebung initialisieren

Eingabecode ausführen

Ausführung

Rückruf

Mikrotasks ausführen

2. Betreten Sie die Ereignisschleife

process.nextTick2.1. Betreten Sie die

Phase

Überprüfen Sie

Ob die Warteschlange abgelaufen ist

Rückrufe, wenn ja, werden alle abgelaufenen Timer Rückrufe in

aufsteigender Reihenfolge ausgeführt
  • , um zu überprüfen, ob Timer Aufgaben vorhanden sind. Wenn ja, alle. Führen Sie TimerTimer aus, um zu überprüfen, ob Mikrotasks vorhanden sind (Versprechen). Wenn ja, führen Sie alle TimerId
  • aus, um den Vorgang zu beenden In dieser Phase
  • process.nextTick
  • 2.2 betreten Sie die Phase
  • und prüfen, ob ein Rückruf für
vorliegt. Wenn ja, wird der Rückruf ausgeführt.

Wenn die Phase nicht beendet wirdPending I/O Callback

    Überprüfen Sie, ob es
  • Aufgaben gibt, wenn ja, führen Sie sie alle ausPending I/O CallbackÜberprüfen Sie, ob es Mikrotasks (Versprechen) gibt, wenn ja , führe sie alle aus
  • Verlasse diese Phase
  • process.nextTick
  • 2.3, betritt die
  • Phase
  • Diese Phase hat wenig mit JavaScript zu tun, überspringe sie
2.4, geben Sie

Phase einidle,prepare

Überprüfen Sie zunächst, ob ein nicht abgeschlossener Rückruf vorliegt. Wenn dieser vorhanden ist, gibt es zwei Situationen:

Fall 1: Es liegt ein ausführbarer Rückruf vor

PollFühren Sie alle verfügbaren Rückrufe aus (einschließlich abgelaufener Timer und einiger IO-Ereignisse usw.)

Überprüfen Sie, ob

Aufgaben vorhanden sind. Wenn ja, führen Sie sie alle aus

Überprüfen Sie, ob vorhanden sind Mikrotasks (Versprechen), wenn ja, alle ausführen

Diese Phase verlassen process.nextTick

Zweiter Fall: Es gibt keinen ausführbaren Rückruf


Überprüfen Sie, ob ein Rückruf, wenn ja, verlassen Sie die Umfragephase. Wenn nicht, blockieren Sie in dieser Phase und warten Sie auf die Benachrichtigung über ein neues Ereignis.

Wenn es keinen unvollendeten Rückruf gibt, verlassen Sie die Umfragephase

2.5, betreten Sie die immediate-Phase

Wenn es einen sofortigen Rückruf gibt, führen Sie alle sofortigen Rückrufe aus


um zu prüfen, ob

Aufgaben vorhanden sind, führen Sie sie alle aus

check

Überprüfen Sie, ob Mikrotasks vorhanden sind (Versprechen). Wenn ja, führen Sie alle aus.

Diese Phase verlassen

process.nextTick2.6, in die
-Phase eintreten

Wenn ein sofortiger Rückruf erfolgt , führe alle sofortigen Rückrufe aus

Prüfe, ob es

Aufgaben gibt, wenn ja, führe sie alle aus

closing Prüfe, ob es Mikrotasks gibt (Versprechen), wenn ja, führe sie alle aus

Verlassen Sie diese Phase

3, prüfen Sie, ob eine aktive vorhanden ist process.nextTick

Wenn ja, fahren Sie mit der nächsten Runde der Ereignisschleife fort

Wenn nicht, beenden Sie die Ereignisschleife und beenden Sie das Programm

handles(定时器、IO等事件句柄)

Hinweis:

Der folgende Prozess wird der Reihe nach vor jeder Unterstufe ausgeführt der Ereignisschleife wird beendet:

Überprüfen Sie, ob ein Process.nextTick-Rückruf vorhanden ist. Wenn ja, werden alle ausgeführt. Überprüfen Sie, ob es Mikrotasks (Versprechen) gibt, und wenn ja, führen Sie sie alle aus.

4.1 关于Promise和process.nextTick

事件循环队列先保证所有的process.nextTick回调,然后将所有的Promise回调追加在后面,最终在每个阶段结束的时候一次性拿出来执行。

此外,process.nextTickPromise回调的数量是受限制的,也就是说,如果一直往这个队列中加入回调,那么整个事件循环就会被卡住

JavaScript-Ausführungsmechanismus und Ereignisschleife in der NodeJS-Umgebung

4.2 关于setTimeout(…, 0) 和 setImmediate

这两个方法的回调到底谁快?

如下面的例子:

setImmediate(() => console.log(2))
setTimeout(() => console.log(1))

使用nodejs多次执行后,发现输出结果有时是1 2,有时是2 1

对于多次执行输出结果不同,需要了解事件循环的基础问题。

首先,Nodejs启动,初始化环境后加载我们的JS代码(index.js).发生了两件事(此时尚未进入消息循环环节):

setImmediate 向 Check 阶段 中添加了回调 console.log(2);

setTimeout 向 Timer 阶段 中添加了回调 console.log(1)

这时候, 要初始化阶段完毕, 要进入 Nodejs 消息循环了。

为什么会有两种输出呢? 接下来一步很关键:

当执行到 Timer 阶段 时, 会发生两种可能. 因为每一轮迭代刚刚进入 Timer 阶段 时会取系统时间保存起来, 以 ms(毫秒) 为最小单位.

如果 Timer 阶段 中回调预设的时间 > 消息循环所保存的时间, 则执行 Timer 阶段 中的该回调. 这种情况下先输出 1, 直到 Check 阶段 执行后,输出2.总的来说, 结果是 1 2.

如果运行比较快, Timer 阶段 中回调预设的时间可能刚好等于消息循环所保存的时间, 这种情况下, Timer 阶段 中的回调得不到执行, 则继续下一个 阶段. 直到 Check 阶段, 输出 2. 然后等下一轮迭代的 Timer 阶段, 这时的时间一定是满足 Timer 阶段 中回调预设的时间 > 消息循环所保存的时间 , 所以 console.log(1) 得到执行, 输出 1. 总的来说, 结果就是 2 1.

所以, 输出不稳定的原因就取决于进入 Timer 阶段 的时间是否和执行 setTimeout 的时间在 1ms 内. 如果把代码改成如下, 则一定会得到稳定的输出:

require('fs').readFile('my-file-path.txt', () => {
 setImmediate(() => console.log(2))
 setTimeout(() => console.log(1))
});

这是因为消息循环在 Pneding I/O Phase 才向 Timer 和 Check 队列插入回调. 这时按照消息循环的执行顺序, Check 一定在 Timer 之前执行。

从性能角度讲, setTimeout 的处理是在 Timer Phase, 其中 min heap 保存了 timer 的回调, 因此每执行一个回调的同时都会涉及到堆调整. 而 setImmediate 仅仅是清空一个队列. 效率自然会高很多.

再从执行时机上讲. setTimeout(..., 0) 和 setImmediate 完全属于两个阶段.

5. 一个实际例子演示

下面以一段代码来说明nodejs运行JavaScript的机制。

如下面一段代码:

setTimeout(() => {                                                // settimeout1
  console.log('1')
  new Promise((resolve) => { console.log('2'); resolve(); })      // Promise3
  .then(() => { console.log('3') })
  new Promise((resolve)=> { console.log('4'); resolve()})         // Promise4
  .then(() => { console.log('5') })
  setTimeout(() => {                                              // settimeout3
    console.log('6')
    setTimeout(() => {                                            // settimeout5
      console.log('7')
      new Promise((resolve) => { console.log('8'); resolve() })   // Promise5
      .then( () => {  console.log('9') })
      new Promise((resolve) => { console.log('10'); resolve() })  // Promise6
      .then(() => {  console.log('11') })
    })
    setTimeout(() => { console.log('12') }, 0)                    // settimeout6
  })
  setTimeout(() => { console.log('13') }, 0)                      // settimeout4
})
setTimeout(() => { console.log('14') }, 0)                        // settimeout2
new Promise((resolve) => { console.log('15'); resolve() })        // Promise1
.then( ()=> { console.log('16') })
new Promise((resolve) => { console.log('17'); resolve() })        // Promise2
.then(() => { console.log('18') })

上面代码执行过程:

node初始化

执行JavaScript代码

遇到setTimeout, 把回调函数放到Timer队列中,记为settimeout1

遇到setTimeout, 把回调函数放到Timer队列中,记为settimeout2

遇到Promise,执行,输出15,把回调函数放到微任务队列,记为Promise1

遇到Promise,执行,输出17,把回调函数放到微任务队列,记为Promise2

代码执行结束,此阶段输出结果:15 17

没有process.nextTick回调,略过

执行微任务

检查微任务队列是否有可执行回调,此时队列有2个回调:Promise1、Promise2

执行Promise1回调,输出16

执行Promise2回调,输出18

此阶段输出结果:16 18

进入第一次事件循环

进入Timer阶段

检查Timer队列是否有可执行的回调,此时队列有2个回调:settimeout1、settimeout2

执行settimeout1回调:

输出1、2、4

添加了2个微任务,记为Promise3、Promise4

添加了2个Timer任务,记为settimeout3、settimeout4

执行settimeout2回调,输出14

Timer队列任务执行完毕

没有process.nextTick回调,略过

检查微任务队列是否有可执行回调,此时队列有2个回调:Promise3、Promise4

按顺序执行2个微任务,输出3、5

此阶段输出结果:1 2 4 14 3 5

Pending I/O Callback阶段没有任务,略过

进入 Poll 阶段

检查是否存在尚未完成的回调,此时有2个回调:settimeout3、settimeout4

Settimeout3-Rückruf ausführen

Ausgabe 6

2 Timer-Aufgaben hinzugefügt, aufgezeichnet als settimeout5, settimeout6

Settimeout4-Rückruf ausführen, Ausgabe 13

Es gibt kein process.nextTick Rückruf, überspringen

Es gibt keine Mikroaufgabe, überspringen

Das Ausgabeergebnis dieser Phase: 6 13

Überprüfen Sie, ob in der Abschlussphase keine Aufgaben vorhanden sind, überspringen Sie

und prüfen Sie, ob noch eine aktive handles(定时器、IO等事件句柄) vorhanden ist. Wenn ja, fahren Sie mit der nächsten Runde der Ereignisschleife fort

und treten Sie in die zweite Ereignisschleife ein

Betreten Sie die Timer-Phase

Überprüfen Sie, ob die Timer-Warteschlange ausführbare Rückrufe enthält. Zu diesem Zeitpunkt verfügt die Warteschlange über zwei Rückrufe: settimeout5, settimeout6

Settimeout5-Rückruf ausführen:

Ausgabe 7, 8, 10

2 Mikrotasks hinzugefügt, aufgezeichnet als Promise5, Promise6

Ausführen settimeout6 Rückruf, Ausgabe 12

hat keinen process.nextTick Rückruf, überspringen Sie

, um zu überprüfen, ob die Mikrotask-Warteschlange ausführbare Rückrufe enthält. Zu diesem Zeitpunkt verfügt die Warteschlange über 2 Rückrufe : Promise5, Promise6

Zwei Mikrotasks nacheinander ausführen, 9, 11 ausgeben

Ergebnisse in dieser Phase ausgeben: 7 8 10 12 9 11

Ausstehender E/A-Rückruf, Abfrage, prüfen, es gibt keine Aufgabe in der Abschlussphase, überspringen

und prüfen, ob noch eine aktive handles(定时器、IO等事件句柄) vorhanden ist. Wenn keine weitere vorhanden ist, beenden Sie das Ereignis Schleife und beenden Sie das Programm.

Die Programmausführung endet und gibt Ergebnis aus: 15 17 16 18 1 2 4 14 3 5 6 13 7 8 10 12 9 11

JavaScript-Ausführungsmechanismus und Ereignisschleife in der NodeJS-Umgebung

[Verwandte Empfehlungen: JavaScript-Video-Tutorial]

Das obige ist der detaillierte Inhalt vonJavaScript-Ausführungsmechanismus und Ereignisschleife in der NodeJS-Umgebung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen