Heim  >  Artikel  >  Web-Frontend  >  Ein Artikel, um mehr über die Ereignisschleife in Node zu erfahren

Ein Artikel, um mehr über die Ereignisschleife in Node zu erfahren

青灯夜游
青灯夜游nach vorne
2021-12-31 19:18:551473Durchsuche

Node.js ist eine Single-Threaded-Sprache, die nicht blockierende E/A-Vorgänge über eine Ereignisschleife abwickelt. Der folgende Artikel vermittelt Ihnen ein detailliertes Verständnis der Ereignisschleife in Node. Ich hoffe, er wird Ihnen hilfreich sein!

Ein Artikel, um mehr über die Ereignisschleife in Node zu erfahren

Node.js Als serverseitige Laufzeit für JavaScript kümmert es sich hauptsächlich um das Netzwerk und Dateien, ohne die Rendering-Phase der Ereignisschleife im Browser.

Im Browser gibt es eine HTML-Spezifikation zur Definition des Ereignisschleifenverarbeitungsmodells, das später von verschiedenen Browserherstellern implementiert wird. Die Definition und Implementierung der Ereignisschleife in Node.js stammen von Libuv.

Libuv basiert auf dem ereignisgesteuerten asynchronen I/O-Modell und wurde ursprünglich für Node.js geschrieben und bietet eine plattformübergreifende Unterstützungsbibliothek. Die folgende Abbildung zeigt die Komponenten, die sich auf die Netzwerkverarbeitung beziehen. Auf der rechten Seite sind auch Dateioperationen und DNS zu sehen Betriebssysteme.

Ein Artikel, um mehr über die Ereignisschleife in Node zu erfahren

Die sechs Phasen der Ereignisschleife

Wenn Node.js startet, initialisiert es die Ereignisschleife, verarbeitet das bereitgestellte Skript, synchroner Code wird zur direkten Ausführung in den Stapel verschoben und asynchrone Aufgaben (Netzwerk) werden ausgeführt Anfragen, Dateioperationen, Timing Nach dem Aufruf der API und der Übergabe der Rückruffunktion wird die Operation zur Verarbeitung durch den Systemkernel in den Hintergrund übertragen. Die meisten aktuellen Kernel sind multithreaded. Wenn einer der Vorgänge abgeschlossen ist, benachrichtigt der Kernel Node.js, die Rückruffunktion zur Abfragewarteschlange hinzuzufügen und auf die Gelegenheit zur Ausführung zu warten.

Die linke Seite des Bildes unten ist die Beschreibung des Ereignisschleifenprozesses auf der offiziellen Website von Node.js und die rechte Seite ist die Beschreibung von Node.js auf der offiziellen Website von Libuv. Beide sind Einführungen in die Ereignisschleife . Nicht jeder kann sich den Quellcode ansehen, um mehr über die Ereignisschleife zu erfahren. Die Einführung auf der offiziellen Website von Node.js kann als Referenz zum Lernen verwendet werden.

Ein Artikel, um mehr über die Ereignisschleife in Node zu erfahren

Die auf der offiziellen Website von Node.js angezeigte Ereignisschleife ist in 6 Stufen unterteilt. Jede Stufe verfügt über eine FIFO-Warteschlange (First In, First Out), in der die Rückruffunktion ausgeführt wird Diese Phasen sind noch klar.

Auf der rechten Seite wird es ausführlicher beschrieben. Bevor Sie die Ereignisschleife durchlaufen, stellen Sie zunächst fest, ob sich die Schleife in einem aktiven Zustand befindet (es gibt wartende asynchrone E/A, Timer usw.). Starten Sie die Iteration, andernfalls wird die Schleife sofort beendet.

Jede Phase wird im Folgenden separat besprochen.

Timer (Timer-Phase)

Zuerst tritt die Ereignisschleife in die Timer-Phase ein, die zwei APIs enthält: setTimeout(cb, ms) und setInterval(cb, ms). Ersteres wird nur einmal ausgeführt und letzteres wird wiederholt implementieren.

In dieser Phase wird geprüft, ob eine abgelaufene Timer-Funktion vorhanden ist. Genau wie im Browser ist die von der Timer-Funktion übergebene Verzögerungszeit immer später als erwartet vom Betriebssystem oder anderen laufenden Callback-Funktionen beeinflusst.

Im folgenden Beispiel stellen wir beispielsweise eine Timer-Funktion ein und erwarten, dass sie nach 1000 Millisekunden ausgeführt wird.

const now = Date.now();
setTimeout(function timer1(){
  log(`delay ${Date.now() - now} ms`);
}, 1000);
setTimeout(function timer2(){
 log(`delay ${Date.now() - now} ms`);
}, 5000);
someOperation();

function someOperation() {
  // sync operation...
  while (Date.now() - now < 3000) {}
}

Nach dem Aufruf der asynchronen Funktion setTimeout führt das Programm dann die Funktion someOperation() aus. Einige zeitaufwändige Vorgänge in der Mitte dauern etwa 3000 ms. Nach Abschluss dieser synchronen Vorgänge tritt es in eine Ereignisschleife ein und prüft zunächst, ob die Timer-Phase abgeschlossen ist Wenn Aufgaben abgelaufen sind, werden Timer-Skripte in aufsteigender Reihenfolge der Verzögerungszeit abgelegt. Wenn nowTime - timerTaskRegisterTime > Andernfalls fahren Sie mit der Überprüfung fort. Nachdem Sie eine Timer-Funktion erreicht haben, die noch nicht abgelaufen ist, oder die maximale Anzahl von Systemabhängigkeiten erreicht haben, fahren Sie mit der nächsten Stufe fort.

Nehmen Sie in unserem Beispiel an, dass die aktuelle Zeit nach der Ausführung der Funktion someOperation() T + 3000 beträgt:

Überprüfen Sie die Funktion timer1. Die aktuelle Zeit beträgt T + 3000 - T > 1000, was die erwartete Verzögerungszeit überschreitet Führen Sie die Callback-Funktion aus und prüfen Sie weiter.

Überprüfen Sie die Timer2-Funktion. Die aktuelle Zeit ist T + 3000 - T

ausstehende Rückrufe

Nachdem die Timer-Phase abgeschlossen ist, tritt die Ereignisschleife in die Phase der ausstehenden Rückrufe ein, in der die E/A-Rückrufe ausgeführt werden, die von der vorherigen Runde der Ereignisschleife übrig geblieben sind. Laut der Libuv-Dokumentation: In den meisten Fällen werden alle E/A-Rückrufe unmittelbar nach der Abfrage der E/A aufgerufen. In einigen Fällen wird der Aufruf solcher Rückrufe jedoch bis zur nächsten Schleifeniteration verschoben. Nach dem Anhören fühlt es sich eher wie ein Vermächtnis der vorherigen Stufe an.

idle, prepare

idle, prepare 阶段是给系统内部使用,idle 这个名字很迷惑,尽管叫空闲,但是在每次的事件循环中都会被调用,当它们处于活动状态时。这一块的资料介绍也不是很多。略...

poll

poll 是一个重要的阶段,这里有一个概念观察者,有文件 I/O 观察者,网络 I/O 观察者等,它会观察是否有新的请求进入,包含读取文件等待响应,等待新的 socket 请求,这个阶段在某些情况下是会阻塞的。

阻塞 I/O 超时时间

在阻塞 I/O 之前,要计算它应该阻塞多长时间,参考 Libuv 文档上的一些描述,以下这些是它计算超时时间的规则:

如果循环使用 UV_RUN_NOWAIT 标志运行、超时为 0。

如果循环将要停止(uv_stop() 被调用),超时为 0。

如果没有活动的 handlers 或 request,超时为 0。

如果有任何 idle handlers 处于活动状态,超时为 0。

如果有任何待关闭的 handlers,超时为 0。

如果以上情况都没有,则采用最近定时器的超时时间,或者如果没有活动的定时器,则超时时间为无穷大,poll 阶段会一直阻塞下去。

示例一

很简单的一段代码,我们启动一个 Server,现在事件循环的其它阶段没有要处理的任务,它会在这里等待下去,直到有新的请求进来。

const http = require(&#39;http&#39;);
const server = http.createServer();
server.on(&#39;request&#39;, req => {
  console.log(req.url);
})
server.listen(3000);

示例二

结合阶段一的定时器,在看个示例,首先启动 app.js 做为服务端,模拟延迟 3000ms 响应,这个只是为了配合测试。再运行 client.js 看下事件循环的执行过程:

首先程序调用了一个在 1000ms 后超时的定时器。

之后调用异步函数 someAsyncOperation() 从网络读取数据,我们假设这个异步网路读取需要 3000ms。

当事件循环开始时先进入 timer 阶段,发现没有超时的定时器函数,继续向下执行。

期间经过 pending callbacks -> idle,prepare 当进入 poll 阶段,此时的 http.get() 尚未完成,它的队列为空,参考上面 poll 阻塞超时时间规则,事件循环机制会检查最快到达阀值的计时器,而不是一直在这里等待下去。

当大约过了 1000ms 后,进入下一次事件循环进入定时器,执行到期的定时器回调函数,我们会看到日志 setTimeout run after 1003 ms。

在定时器阶段结束之后,会再次进入 poll 阶段,继续等待。

// client.js
const now = Date.now();
setTimeout(() => log(`setTimeout run after ${Date.now() - now} ms`), 1000);
someAsyncOperation();
function someAsyncOperation() {
  http.get(&#39;http://localhost:3000/api/news&#39;, () => {
    log(`fetch data success after ${Date.now() - now} ms`);
  });
}

// app.js
const http = require(&#39;http&#39;);
http.createServer((req, res) => {
  setTimeout(() => { res.end(&#39;OK!&#39;) }, 3000);
}).listen(3000);

当 poll 阶段队列为空时,并且脚本被 setImmediate() 调度过,此时,事件循环也会结束 poll 阶段,进入下一个阶段 check。

check

check 阶段在 poll 阶段之后运行,这个阶段包含一个 API setImmediate(cb) 如果有被 setImmediate 触发的回调函数,就取出执行,直到队列为空或达到系统的最大限制。

setTimeout VS setImmediate

拿 setTimeout 和 setImmediate 对比,这是一个常见的例子,基于被调用的时机和定时器可能会受到计算机上其它正在运行的应用程序影响,它们的输出顺序,不总是固定的。

setTimeout(() => log(&#39;setTimeout&#39;));
setImmediate(() => log(&#39;setImmediate&#39;));

// 第一次运行
setTimeout
setImmediate

// 第二次运行
setImmediate
setTimeout

setTimeout VS setImmediate VS fs.readFile

但是一旦把这两个函数放入一个 I/O 循环内调用,setImmediate 将总是会被优先调用。因为 setImmediate 属于 check 阶段,在事件循环中总是在 poll 阶段结束后运行,这个顺序是确定的。

fs.readFile(__filename, () => {
  setTimeout(() => log(&#39;setTimeout&#39;));
  setImmediate(() => log(&#39;setImmediate&#39;));
})

close callbacks

在 Libuv 中,如果调用关闭句柄 uv_close(),它将调用关闭回调,也就是事件循环的最后一个阶段 close callbacks。

这个阶段的工作更像是做一些清理工作,例如,当调用 socket.destroy(),'close' 事件将在这个阶段发出,事件循环在执行完这个阶段队列里的回调函数后,检查循环是否还 alive,如果为 no 退出,否则继续下一次新的事件循环。

包含 Microtask 的事件循环流程图

在浏览器的事件循环中,把任务划分为 Task、Microtask,在 Node.js 中是按照阶段划分的,上面我们介绍了 Node.js 事件循环的 6 个阶段,给用户使用的主要是 timer、poll、check、close callback 四个阶段,剩下两个由系统内部调度。这些阶段所产生的任务,我们可以看做 Task 任务源,也就是常说的 “Macrotask 宏任务”。

通常我们在谈论一个事件循环时还会包含 Microtask,Node.js 里的微任务有 Promise、还有一个也许很少关注的函数 queueMicrotask,它是在 Node.js v11.0.0 之后被实现的,参见 PR/22951。

Node.js 中的事件循环在每一个阶段执行后,都会检查微任务队列中是否有待执行的任务。

Ein Artikel, um mehr über die Ereignisschleife in Node zu erfahren

Node.js 11.x 前后差异

Node.js 在 v11.x 前后,每个阶段如果即存在可执行的 Task 又存在 Microtask 时,会有一些差异,先看一段代码:

setImmediate(() => {
  log(&#39;setImmediate1&#39;);
  Promise.resolve(&#39;Promise microtask 1&#39;)
    .then(log);
});
setImmediate(() => {
  log(&#39;setImmediate2&#39;);
  Promise.resolve(&#39;Promise microtask 2&#39;)
    .then(log);
});

在 Node.js v11.x 之前,当前阶段如果存在多个可执行的 Task,先执行完毕,再开始执行微任务。基于 v10.22.1 版本运行结果如下:

setImmediate1
setImmediate2
Promise microtask 1
Promise microtask 2

在 Node.js v11.x 之后,当前阶段如果存在多个可执行的 Task,先取出一个 Task 执行,并清空对应的微任务队列,再次取出下一个可执行的任务,继续执行。基于 v14.15.0 版本运行结果如下:

setImmediate1
Promise microtask 1
setImmediate2
Promise microtask 2

在 Node.js v11.x 之前的这个执行顺序问题,被认为是一个应该要修复的 Bug 在 v11.x 之后并修改了它的执行时机,和浏览器保持了一致,详细参见 issues/22257 讨论。

特别的 process.nextTick()

Node.js 中还有一个异步函数 process.nextTick(),从技术上讲它不是事件循环的一部分,它在当前操作完成后处理。如果出现递归的 process.nextTick() 调用,这将会很糟糕,它会阻断事件循环。

如下例所示,展示了一个 process.nextTick() 递归调用示例,目前事件循环位于 I/O 循环内,当同步代码执行完成后 process.nextTick() 会被立即执行,它会陷入无限循环中,与同步的递归不同的是,它不会触碰 v8 最大调用堆栈限制。但是会破坏事件循环调度,setTimeout 将永远得不到执行。

fs.readFile(__filename, () => {
  process.nextTick(() => {
    log(&#39;nextTick&#39;);
    run();
    function run() {
      process.nextTick(() => run());
    }
  });
  log(&#39;sync run&#39;);
  setTimeout(() => log(&#39;setTimeout&#39;));
});

// 输出
sync run
nextTick

将 process.nextTick 改为 setImmediate 虽然是递归的,但它不会影响事件循环调度,setTimeout 在下一次事件循环中被执行。

fs.readFile(__filename, () => {
  process.nextTick(() => {
    log(&#39;nextTick&#39;);
    run();
    function run() {
      setImmediate(() => run());
    }
  });
  log(&#39;sync run&#39;);
  setTimeout(() => log(&#39;setTimeout&#39;));
});

// 输出
sync run
nextTick
setTimeout

process.nextTick 是立即执行,setImmediate 是在下一次事件循环的 check 阶段执行。但是,它们的名字着实让人费解,也许会想这两个名字交换下比较好,但它属于遗留问题,也不太可能会改变,因为这会破坏 NPM 上大部分的软件包。

在 Node.js 的文档中也建议开发者尽可能的使用 setImmediate(),也更容易理解。

更多node相关知识,请访问:nodejs 教程!!

Das obige ist der detaillierte Inhalt vonEin Artikel, um mehr über die Ereignisschleife in Node zu erfahren. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

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