Heim  >  Artikel  >  Web-Frontend  >  Lassen Sie uns über die Ereignisschleife in Node sprechen

Lassen Sie uns über die Ereignisschleife in Node sprechen

青灯夜游
青灯夜游nach vorne
2023-04-11 19:08:211524Durchsuche

Die Ereignisschleife ist ein grundlegender Bestandteil von Node.js. Sie ermöglicht die asynchrone Programmierung, indem sie sicherstellt, dass der Hauptthread nicht blockiert wird. Das Verständnis der Ereignisschleife ist für die Erstellung effizienter Anwendungen von entscheidender Bedeutung. Der folgende Artikel wird Ihnen ein detailliertes Verständnis der Ereignisschleife in Node vermitteln. Ich hoffe, er wird Ihnen hilfreich sein!

Lassen Sie uns über die Ereignisschleife in Node sprechen

Sie verwenden Node.js schon seit einiger Zeit, haben einige Apps erstellt, verschiedene Module ausprobiert und fühlen sich sogar mit der asynchronen Programmierung vertraut. Aber etwas stört Sie immer wieder – die Ereignisschleife.

Wenn Sie wie ich sind, haben Sie unzählige Stunden damit verbracht, Dokumentationen zu lesen und Videos anzuschauen, um die Ereignisschleife zu verstehen. Aber selbst als erfahrener Entwickler kann es sein, dass Sie Schwierigkeiten haben, die Funktionsweise vollständig zu verstehen. Aus diesem Grund habe ich diesen visuellen Leitfaden vorbereitet, um Ihnen zu helfen, die Node.js-Ereignisschleife vollständig zu verstehen. Lehnen Sie sich also zurück, trinken Sie eine Tasse Kaffee und tauchen Sie ein in die Welt der Node.js-Ereignisschleife. [Verwandte Tutorial-Empfehlungen: nodejs-Video-Tutorial, Programmierunterricht]

Asynchrone Programmierung in JavaScript

Wir beginnen mit einem Überblick über die asynchrone Programmierung in JavaScript. Während JavaScript in Web-, Mobil- und Desktop-Anwendungen verwendet wird, ist es wichtig zu bedenken, dass JavaScript im Kern eine synchrone, blockierende Single-Thread-Sprache ist. Lassen Sie uns diesen Satz anhand eines kurzen Codeausschnitts verstehen.

// index.js

function A() {
  console.log("A");
}

function B() {
  console.log("B");
}

A()
B()

// Logs A and then B

JavaScript ist synchron

Wenn wir zwei Funktionen haben, die Nachrichten in der Konsole protokollieren, wird der Code von oben nach unten Zeile für Zeile ausgeführt. Im obigen Codeausschnitt sehen wir, dass A vor B aufgezeichnet wird.

JavaScript blockiert

JavaScript blockiert aufgrund seiner synchronen Natur. Unabhängig davon, wie lange der vorherige Prozess dauert, werden nachfolgende Prozesse erst gestartet, wenn der erstere abgeschlossen ist. Wenn Funktion A im Codeausschnitt einen großen Codeblock ausführen muss, muss JavaScript diesen Vorgang abschließen, ohne zu Funktion B zu verzweigen. Auch wenn dieser Code 10 Sekunden oder sogar eine Minute dauert.

Möglicherweise ist Ihnen diese Situation in Ihrem Browser schon einmal begegnet. Wenn eine Webanwendung im Browser ausgeführt wird und einige intensive Codeblöcke ausführt, ohne die Kontrolle an den Browser zurückzugeben, kann es sein, dass der Browser einfriert, was als Blockierung bezeichnet wird. Der Browser wird daran gehindert, weiterhin Benutzereingaben zu verarbeiten und andere Aufgaben auszuführen, bis die Webanwendung die Prozessorsteuerung an den Browser zurückgibt.

JavaScript ist Single-Threaded

Ein Thread ist der Prozess, den Ihr JavaScript-Programm zum Ausführen von Aufgaben verwenden kann. Jeder Thread kann jeweils nur eine Aufgabe ausführen. Im Gegensatz zu anderen Sprachen, die Multithreading unterstützen und mehrere Aufgaben gleichzeitig ausführen können, verfügt JavaScript nur über einen Thread, den sogenannten Hauptthread, der Code ausführt.

Warten auf JavaScript

Wie Sie sich vorstellen können, verursacht dieses JavaScript-Modell Probleme, da wir auf den Abruf der Daten warten müssen, bevor wir mit der Ausführung des Codes fortfahren können. Diese Wartezeit kann mehrere Sekunden dauern, in denen wir keinen anderen Code ausführen können. Wenn JavaScript die Verarbeitung fortsetzt, ohne zu warten, tritt ein Fehler auf. Wir müssen asynchrones Verhalten in JavaScript implementieren. Gehen wir zu Node.js und werfen einen Blick darauf.

Node.js Runtime

Lassen Sie uns über die Ereignisschleife in Node sprechen

Die Node.js Runtime ist eine Umgebung, die es Ihnen ermöglicht, JavaScript-Programme ohne Verwendung eines Browsers zu verwenden und auszuführen. Core – die Node-Laufzeit, besteht aus drei Hauptkomponenten.

  • Externe Abhängigkeiten – wie V8, libuv, crypto usw. – sind erforderliche Funktionen von Node.js.
  • C++-Funktionen bieten Funktionen wie Dateisystemzugriff und Netzwerk.
  • Die JavaScript-Bibliothek bietet Funktionen und Tools zum einfachen Aufrufen von C++-Funktionen mithilfe von JavaScript-Code.

Während alle Teile wichtig sind, ist libuv die Schlüsselkomponente der asynchronen Programmierung in Node.js.

Libuv

Libuv ist eine plattformübergreifende Open-Source-Bibliothek, die in der Sprache C geschrieben ist. In der Node.js-Laufzeit besteht seine Rolle darin, Unterstützung für die Verarbeitung asynchroner Vorgänge bereitzustellen. Werfen wir einen Blick darauf, wie es funktioniert.

Codeausführung in der Node.js-Laufzeit

Lassen Sie uns über die Ereignisschleife in Node sprechen

让我们来概括一下代码在 Node 运行时中的执行方式。在执行代码时,位于图片左侧的 V8 引擎负责 JavaScript 代码的执行。该引擎包含一个内存堆(Memory heap)和一个调用栈(Call stack)。

每当声明变量或函数时,都会在堆上分配内存。执行代码时,函数就会被推入调用栈中。当函数返回时,它就从调用栈中弹出了。这是对栈数据结构的简单实现,最后添加的项是第一个被移除。在图片右侧,是负责处理异步方法的 libuv。

每当我们执行异步方法时,libuv 接管任务的执行。然后使用操作系统本地异步机制运行任务。如果本地机制不可用或不足,则利用其线程池来运行任务,并确保主线程不被阻塞。

同步代码执行

首先,让我们来看一下同步代码执行。以下代码由三个控制台日志语句组成,依次记录“First”,“Second”和“Third”。我们按照运行时执行顺序来查看代码。

// index.js
console.log("First");
console.log("Second");
console.log("Third");

以下是 Node 运行时执行同步代码的可视化展示。

Lassen Sie uns über die Ereignisschleife in Node sprechen

执行的主线程始终从全局作用域开始。全局函数(如果我们可以这样称呼它)被推入堆栈中。然后,在第 1 行,我们有一个控制台日志语句。这个函数被推入堆栈中。假设这个发生在 1 毫秒时,“First” 被记录在控制台上。然后,这个函数从堆栈中弹出。

执行到第 2 行时。假设到第 2 毫秒了,log 函数再次被推入堆栈中。“Second”被记录在控制台上,并弹出该函数。

最后,执行到第 3 行了。第 3 毫秒时,log 函数被推入堆栈,“Third”将记录在控制台上,并弹出该函数。此时已经没有代码要执行,全局也被弹出。

异步代码执行

接下来,让我们看一下异步代码执行。有以下代码片段:包含三个日志语句,但这次第二个日志语句传递给了fs.readFile() 作为回调函数。

Lassen Sie uns über die Ereignisschleife in Node sprechen

执行的主线程始终从全局作用域开始。全局函数被推入堆栈。然后执行到第 1 行,在第 1 毫秒时,“First”被记录在控制台中,并弹出该函数。然后执行移动到第 2 行,在第 2毫秒时,readFile 方法被推入堆栈。由于 readFile 是异步操作,因此它会转移(off-loaded)到 libuv。

JavaScript 从调用堆栈中弹出了 readFile 方法,因为就第 2 行的执行而言,它的工作已经完成了。在后台,libuv 开始在单独的线程上读取文件内容。在第 3 毫秒时,JavaScript 继续进行到第 5 行,将 log 函数推入堆栈,“Third”被记录到控制台中,并将该函数弹出堆栈。

大约在第 4 毫秒左右,假设文件读取任务已经完成,则相关回调函数现在会在调用栈上执行, 在回调函数内部遇到 log 函数。

log 函数推入到到调用栈,“Second”被记录到控制台并弹出 log 函数 。由于回调函数中没有更多要执行的语句,因此也被弹出 。没有更多代码可运行了 ,所以全局函数也从堆栈中删除 。

控制台输出“First”,“Third”,然后是“Second”。

Libuv 和异步操作

很明显,libuv 用于处理 Node.js 中的异步操作。对于像处理网络请求这样的异步操作,libuv 依赖于操作系统原生机制。对于没有本地 OS 支持的异步读取文件的操作,libuv 则依赖其线程池以确保主线程不被阻塞。然而,这也引发了一些问题。

  • 当一个异步任务在 libuv 中完成时,什么时候 Node 会在调用栈上运行相关联的回调函数?
  • Node 是否会等待调用栈为空后再运行回调函数?还是打断正常执行流来运行回调函数?
  • setTimeoutsetInterval 这类延迟执行回调函数的方法又是何时执行回调函数呢?
  • 如果 setTimeoutreadFile 这类异步任务同时完成,Node 如何决定哪个回调函数先在调用栈上运行?其中一个会有更多的优先级吗?

所有这些问题都可以通过理解 libuv 核心部分——事件循环来得到答案。

Was ist eine Ereignisschleife?

Technisch gesehen ist die Ereignisschleife nur ein C-Sprachprogramm. Aber in Node.js können Sie es sich als Entwurfsmuster zum Koordinieren der Ausführung von synchronem und asynchronem Code vorstellen.

Visuelle Ereignisschleife

Die Ereignisschleife ist eine Schleife, die so lange läuft, wie Ihre Node.js-Anwendung ausgeführt wird. In jeder Schleife gibt es sechs verschiedene Warteschlangen, die jeweils eine oder mehrere Rückruffunktionen enthalten, die schließlich auf dem Aufrufstapel ausgeführt werden müssen.

Lassen Sie uns über die Ereignisschleife in Node sprechen

  • Zuallererst gibt es eine Timer-Warteschlange (technisch Min-Heap genannt), die in Bezug auf die Rückruffunktionen setTimeout und setInterval gespeichert wird.
  • setTimeoutsetInterval 相关的回调函数。
  • 其次,有一个 I/O 队列(I/O queue),其中包含与所有异步方法相关的回调函数,例如 fshttp 模块中提供的相关方法。
  • 第三个是检查队列(check queue),它保存与 setImmediate 函数相关的回调函数,这是特定于Node 的功能。
  • 第四个是关闭队列(close queue),它保存与异步任务关闭事件相关联的回调函数。

最后,有两个不同队列组成微任务队列(microtask queue)。

  • nextTick 队列保存了与 process.nextTick 函数关联的回调函数。
  • Promise 队列则保存了JavaScript 中本地 Promise 相关联的回调函数。

需要注意的是计时器、I/O、检查和关闭队列都属于 libuv。然而,两个微任务队列并不属于 libuv。尽管如此,它们仍然是 Node 运行时环境中扮演着重要角色,并且在执行回调顺序方面发挥着重要作用。说到这里, 让我们来理解一下事件循环是如何工作的。

事件循环是如何工作的?

图中箭头是一个提示,但可能还不太容易理解。让我来解释一下队列的优先级顺序。首先要知道,所有用户编写的同步 JavaScript 代码都比异步代码优先级更高。这表示只有在调用堆栈为空时,事件循环才会发挥作用。

在事件循环中,执行顺序遵循某些规则。需要掌握的规则还是有一些的,我们逐个的了解一下:

  1. 执行微任务队列(microtask queue)中的所有回调函数。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务。
  2. 执行计时器队列(timer queue)内的所有回调函数。
  3. 如果微任务队列中存在回调函数,则在计时器队列内每执行完一次回调函数之后执行微任务队列中的所有回调函数。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务。
  4. 执行 I/O 队列(I/O queue)内的所有回调函数。
  5. 如果微任务队列中存在回调函数,按照先 nextTick 队列后 Promise 队列的顺序依次执行微任务队列中的所有回调函数。
  6. 执行检查队列(check queue)内的所有回调函数。
  7. 如果微任务队列中存在回调函数,则在检查队列内每个回调之后执行微任务队列中的所有回调函数 。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务。
  8. 执行关闭队列(close queue)内的所有回调函数。
  9. 在同一循环的最后,再执行一次微任务队列。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务。

此时,如果还有更多的回调需要处理,那么事件循环再运行一次(译注:事件循环在程序运行期间一直在运行,在当前没有可供处理的任务情况下,会处于等待状态,一旦有新任务就会执行),并重复相同的步骤。另一方面,如果所有回调都已执行并且没有更多代码要处理(译注:也就是程序执行结束),则事件循环退出。

这就是 libuv 事件循环在 Node.js 中执行异步代码的作用。有了这些规则,我们可以重新审视之前提出的问题。


当一个异步任务在 libuv 中完成时,什么时候 Node 会在调用栈上运行相关联的回调函数?

答案:只有当调用栈为空时才执行回调函数。

Node 是否会等待调用栈为空后再运行回调函数?还是打断正常执行流来运行回调函数?

答案:运行回调函数时不会打断正常执行流。

setTimeoutsetIntervalZweitens gibt es eine E/A-Warteschlange, die Rückruffunktionen für alle asynchronen Methoden enthält, z. B. fs- und http-Module. Verwandte Methoden, die in bereitgestellt werden. Die dritte ist die Prüfwarteschlange, die die Rückruffunktion im Zusammenhang mit der Funktion setImmediate enthält, bei der es sich um eine knotenspezifische Funktion handelt.

Die vierte ist die Schließwarteschlange, die die Rückruffunktion speichert, die mit dem asynchronen Aufgabenschließereignis verknüpft ist. 🎜Schließlich gibt es zwei verschiedene Warteschlangen, die die Mikrotask-Warteschlange bilden. 🎜🎜🎜nextTick-Warteschlange speichert die Rückruffunktion, die mit der Funktion process.nextTick verknüpft ist. 🎜Die Promise-Warteschlange speichert die Rückruffunktionen, die mit lokalem Promise in JavaScript verknüpft sind. 🎜Es ist zu beachten, dass Timer, E/A, Prüf- und Schließwarteschlangen alle zu libuv gehören. Die beiden Mikrotask-Warteschlangen gehören jedoch nicht zu libuv. Dennoch spielen sie immer noch eine wichtige Rolle in der Node-Laufzeitumgebung und spielen eine wichtige Rolle bei der Reihenfolge, in der Rückrufe ausgeführt werden. Lassen Sie uns jedoch verstehen, wie die Ereignisschleife funktioniert. 🎜

🎜Wie funktioniert die Ereignisschleife? 🎜🎜🎜Der Pfeil im Bild ist ein Hinweis, aber möglicherweise nicht leicht zu verstehen. Lassen Sie mich die Reihenfolge der Warteschlangenpriorität erklären. Das Erste, was Sie wissen müssen, ist, dass der gesamte vom Benutzer geschriebene synchrone JavaScript-Code Vorrang vor asynchronem Code hat. Dies bedeutet, dass die Ereignisschleife nur funktioniert, wenn der Aufrufstapel leer ist. 🎜🎜In der Ereignisschleife folgt die Reihenfolge der Ausführung bestimmten Regeln. Es gibt noch einige Regeln, die gemeistert werden müssen. Schauen wir sie uns einzeln an: 🎜
    🎜Führen Sie alle Rückruffunktionen in der Mikrotask-Warteschlange aus. Zuerst die Aufgaben in der nextTick-Warteschlange, dann die Aufgaben in der Promise-Warteschlange. 🎜Führen Sie alle Rückruffunktionen in der Timer-Warteschlange aus. 🎜Wenn es eine Rückruffunktion in der Mikrotask-Warteschlange gibt, werden alle Rückruffunktionen in der Mikrotask-Warteschlange ausgeführt, nachdem jede Rückruffunktion in der Timer-Warteschlange ausgeführt wurde🎜. Zuerst die Aufgaben in der nextTick-Warteschlange, dann die Aufgaben in der Promise-Warteschlange. 🎜Führen Sie alle Rückruffunktionen in der E/A-Warteschlange aus. 🎜Wenn Rückruffunktionen in der Mikrotask-Warteschlange vorhanden sind, werden alle Rückruffunktionen in der Mikrotask-Warteschlange nacheinander in der Reihenfolge der NextTick-Warteschlange und dann der Promise-Warteschlange ausgeführt. 🎜Führen Sie alle Rückruffunktionen in der Prüfwarteschlange aus. 🎜Wenn es eine Rückruffunktion in der Mikrotask-Warteschlange gibt, werden alle Rückruffunktionen in der Mikrotask-Warteschlange ausgeführt, nachdem 🎜jeder Rückruf in der Warteschlange überprüft wurde. Zuerst die Aufgaben in der nextTick-Warteschlange, dann die Aufgaben in der Promise-Warteschlange. 🎜Führen Sie alle Rückruffunktionen in der Schließwarteschlange aus. 🎜Führen Sie am Ende derselben Schleife die Mikrotask-Warteschlange erneut aus. Zuerst die Aufgaben in der nextTick-Warteschlange, dann die Aufgaben in der Promise-Warteschlange.
🎜Wenn zu diesem Zeitpunkt weitere Rückrufe verarbeitet werden müssen, wird die Ereignisschleife erneut ausgeführt (Anmerkung: Die Ereignisschleife läuft weiter, während das Programm ausgeführt wird, und derzeit sind keine Aufgaben zu verarbeiten. Als nächstes befindet es sich im Wartezustand und wird ausgeführt, sobald eine neue Aufgabe vorliegt) und wiederholen Sie die gleichen Schritte. Wenn andererseits alle Rückrufe ausgeführt wurden und kein Code mehr verarbeitet werden muss, wird die Ereignisschleife beendet. 🎜🎜Dies ist, was die libuv-Ereignisschleife für die Ausführung von asynchronem Code in Node.js tut. Mit diesen Regeln können wir die Fragen, die wir zuvor gestellt haben, noch einmal durchgehen. 🎜
🎜🎜Wenn eine asynchrone Aufgabe in libuv abgeschlossen ist, wann führt Node die zugehörige Rückruffunktion auf dem Aufrufstapel aus? 🎜🎜🎜Antwort: Die Callback-Funktion wird nur ausgeführt, wenn der Call-Stack leer ist. 🎜🎜🎜 Wird Node warten, bis der Aufrufstapel leer ist, bevor er die Rückruffunktion ausführt? Oder den normalen Ausführungsablauf unterbrechen, um die Rückruffunktion auszuführen? 🎜🎜🎜Antwort:Das Ausführen einer Callback-Funktion unterbricht den normalen Ausführungsfluss nicht. 🎜🎜🎜Methoden wie setTimeout und setInterval, die die Ausführung von Rückruffunktionen verzögern. Wann wird die Rückruffunktion ausgeführt? 🎜🎜

Antwort: setTimeout und setInterval werden mit der ersten Priorität unter allen Rückruffunktionen ausgeführt (unabhängig von der Mikrotask-Warteschlange). setTimeoutsetInterval 的所有回调函数中第一优先级执行的(不考虑微任务队列)。

如果两个异步任务(例如 setTimeoutreadFile

Wenn zwei asynchrone Aufgaben (z. B. setTimeout und readFile) gleichzeitig abgeschlossen werden, wie entscheidet Node, welche Rückruffunktion zuerst im Aufrufstapel ausgeführt wird? Hat einer eine höhere Priorität als der andere?

Antwort:
Bei gleichzeitiger Fertigstellung wird der Timer-Rückruf vor dem I/O-Rückruf ausgeführt.

Lassen Sie uns über die Ereignisschleife in Node sprechenWir haben bisher viel gelernt, aber ich hoffe, Sie können sich die im Bild unten gezeigte Ausführungssequenz merken, denn sie zeigt vollständig, wie Node.js asynchronen Code hinter den Kulissen ausführt.

Aber Sie fragen sich vielleicht: „Wo ist der Code zur Überprüfung dieser Visualisierung?“ Nun, jede Warteschlange in der Ereignisschleife weist Implementierungsnuancen auf, daher sollten wir sie besser einzeln besprechen. Dieser Artikel ist der erste einer Reihe über die Node.js-Ereignisschleife. Bitte überprüfen Sie unbedingt den Link am Ende des Artikels, um die Details des Vorgangs in jeder Warteschlange zu verstehen. Auch wenn Sie jetzt einen tiefen Eindruck haben, tappen Sie möglicherweise immer noch in einige Fallen, wenn Sie zu dem spezifischen Szenario gelangen .

Fazit

Dieser visuelle Leitfaden behandelt die Grundlagen der asynchronen Programmierung in JavaScript, der Node.js-Laufzeitumgebung und libuv, das asynchrone Vorgänge verarbeitet. Mit diesem Wissen können Sie ein leistungsstarkes Ereignisschleifenmodell erstellen, das beim Schreiben von Code, der die asynchrone Natur von Node.js nutzt, von Vorteil ist.

Weitere Informationen zu Knoten finden Sie unter: 🎜nodejs-Tutorial🎜! 🎜

Das obige ist der detaillierte Inhalt vonLassen Sie uns über die Ereignisschleife in Node sprechen. 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