Wie funktioniert die Ereignisschleife in JavaScript?

Während es möglicherweise ein tiefgreifendes Verständnis von Sprachen wie C++ und C erfordert, um vollständigen Produktionscode zu schreiben, kann JavaScript oft mit nur einem grundlegenden Verständnis dessen geschrieben werden, was mit der Sprache gemacht werden kann.

Konzepte wie das Übergeben von Rückrufen an Funktionen oder das Schreiben von asynchronem Code sind oft nicht so schwierig zu implementieren, weshalb sich die meisten JavaScript-Entwickler weniger darum kümmern, was unter der Haube vor sich geht. Sie kümmern sich einfach nicht darum, die Komplexität zu verstehen, die ihnen durch die Sprache zutiefst abstrahiert wurde.

Als JavaScript-Entwickler wird es immer wichtiger zu verstehen, was wirklich unter der Haube passiert und wie die meisten dieser von uns abstrahierten Komplexitäten wirklich funktionieren. Es hilft uns, fundiertere Entscheidungen zu treffen, was wiederum unsere Codeleistung drastisch steigern kann.

Dieser Artikel konzentriert sich auf eines der sehr wichtigen, aber selten verstandenen Konzepte oder Begriffe in JavaScript. Die EREIGNISSCHLEIFE!.

Das Schreiben von asynchronem Code lässt sich in JavaScript nicht vermeiden, aber warum bedeutet asynchron laufender Code eigentlich? dh die Ereignisschleife

Bevor wir verstehen können, wie die Ereignisschleife funktioniert, müssen wir zuerst verstehen, was JavaScript selbst ist und wie es funktioniert!

Was ist JavaScript?

Bevor wir fortfahren, möchte ich, dass wir einen Schritt zurück zu den Grundlagen machen. Was ist eigentlich JavaScript? Wir könnten JavaScript definieren als;

JavaScript ist eine hochrangige, interpretierte, Single-Threaded, nicht blockierende, asynchrone, nebenläufige Sprache.

Warte, was ist das? Eine buchstäbliche Definition? 🤔

Brechen wir es ab!

Die Schlüsselwörter hier in Bezug auf diesen Artikel sind Single-Threaded, Non-Blocking, Concurrent und Asynchronous.

Einzelner Thread

Ein Ausführungs-Thread ist die kleinste Sequenz programmierter Anweisungen, die von einem Scheduler unabhängig verwaltet werden kann. Eine Programmiersprache ist Single-Threaded, was bedeutet, dass sie nur eine Aufgabe oder Operation gleichzeitig ausführen kann. Dies bedeutet, dass ein vollständiger Prozess von Anfang bis Ende ausgeführt wird, ohne dass der Thread unterbrochen oder gestoppt wird.

Im Gegensatz zu Multithreading-Sprachen, bei denen mehrere Prozesse gleichzeitig auf mehreren Threads ausgeführt werden können, ohne sich gegenseitig zu blockieren.

Wie kann JavaScript gleichzeitig Single-Threaded und nicht blockierend sein?

Aber was heißt blockieren?

Nicht blockierend

Es gibt keine einheitliche Definition des Blockierens; es bedeutet einfach Dinge, die langsam auf dem Thread laufen. Non-Blocking bedeutet also Dinge, die im Thread nicht langsam sind.

Aber warte, habe ich gesagt, dass JavaScript in einem einzigen Thread ausgeführt wird? Und ich sagte auch, dass es nicht blockierend ist, was bedeutet, dass die Aufgabe schnell auf dem Aufrufstapel ausgeführt wird? Aber wie??? Wie wäre es, wenn wir Timer laufen lassen? Schleifen?

Entspannen! Wir würden es gleich herausfinden 😉.

Gleichzeitig

Parallelität bedeutet, dass der Code gleichzeitig von mehr als einem Thread ausgeführt wird.

Okay, die Dinge werden wirklich seltsam Nun, wie kann JavaScript Single-Threaded und gleichzeitig gleichzeitig sein? dh seinen Code mit mehr als einem Thread ausführen?

Asynchron

Asynchrone Programmierung bedeutet, dass der Code in einer Ereignisschleife ausgeführt wird. Bei einem Sperrvorgang wird das Ereignis gestartet. Der Blockierungscode wird weiter ausgeführt, ohne den Hauptausführungsthread zu blockieren. Wenn der Blockierungscode seine Ausführung beendet hat, stellt er das Ergebnis der Blockierungsoperationen in die Warteschlange und schiebt sie zurück auf den Stack.

Aber JavaScript hat einen einzigen Thread? Was führt dann diesen blockierenden Code aus, während andere Codes im Thread ausgeführt werden?

Bevor wir fortfahren, lassen Sie uns das oben Gesagte noch einmal zusammenfassen.

  • JavaScript ist Single-Threaded
  • JavaScript ist nicht blockierend, dh langsame Prozesse blockieren seine Ausführung nicht
  • JavaScript ist nebenläufig, dh es führt seinen Code in mehr als einem Thread gleichzeitig aus
  • JavaScript ist asynchron, dh es führt an anderer Stelle blockierenden Code aus.

Aber das Obige passt nicht genau zusammen, wie kann eine Singlethread-Sprache nicht blockierend, gleichzeitig und asynchron sein?

Lassen Sie uns etwas tiefer gehen, gehen wir zu den JavaScript-Laufzeit-Engines, V8, vielleicht hat es einige versteckte Threads, von denen wir nichts wissen.

V8-Motor

Die V8-Engine ist eine leistungsstarke Open-Source-Webassembly-Laufzeit-Engine für JavaScript, die von Google in C++ geschrieben wurde. Die meisten Browser führen JavaScript mit der V8-Engine aus, und sogar die beliebte Node-js-Laufzeitumgebung verwendet sie ebenfalls.

Im einfachen Englisch ist V8 ein C++-Programm, das JavaScript-Code empfängt, kompiliert und ausführt.

Der V8 macht zwei wichtige Dinge;

  • Heap-Speicherzuordnung
  • Aufruf-Stack-Ausführungskontext

Leider war unser Verdacht falsch. Der V8 hat nur einen Call-Stack, stellen Sie sich den Call-Stack als den Thread vor.

Ein Thread === ein Call-Stack === jeweils eine Ausführung.

Bild – Hacker Mittag

Da V8 nur einen Aufrufstapel hat, wie läuft dann JavaScript gleichzeitig und asynchron, ohne den Hauptausführungsthread zu blockieren?

Versuchen wir es herauszufinden, indem wir einen einfachen, aber gemeinsamen asynchronen Code schreiben und ihn gemeinsam analysieren.

JavaScript führt jeden Code Zeile für Zeile nacheinander aus (Single-Threaded). Wie erwartet wird hier die erste Zeile in der Konsole gedruckt, aber warum wird die letzte Zeile vor dem Timeout-Code gedruckt? Warum wartet der Ausführungsprozess nicht auf den Timeout-Code (Blockierung), bevor er mit der Ausführung der letzten Zeile fortfährt?

Ein anderer Thread scheint uns geholfen zu haben, dieses Timeout auszuführen, da wir uns ziemlich sicher sind, dass ein Thread zu jedem Zeitpunkt nur eine einzige Aufgabe ausführen kann.

Werfen wir einen kurzen Blick in die V8-Quellcode für eine Weile.

Warte was??!!! Es gibt keine Timer-Funktionen in V8, kein DOM? Keine Ereignisse? Kein AJAX?…. Yeeeesss!!!

Ereignisse, DOM, Timer usw. sind nicht Teil der Kernimplementierung von JavaScript, JavaScript entspricht strikt den Ecma Scripts-Spezifikationen und verschiedene Versionen davon werden oft gemäß den Ecma Scripts-Spezifikationen (ES X) bezeichnet.

Ausführungs-Workflow

Ereignisse, Timer und Ajax-Anforderungen werden alle clientseitig von den Browsern bereitgestellt und oft als Web-API bezeichnet. Sie sind diejenigen, die es dem Single-Thread-JavaScript ermöglichen, nicht blockierend, gleichzeitig und asynchron zu sein! Aber wie?

Der Ausführungs-Workflow eines JavaScript-Programms besteht aus drei Hauptabschnitten: dem Call-Stack, der Web-API und der Task-Warteschlange.

Der Call-Stack

Ein Stack ist eine Datenstruktur, bei der das zuletzt hinzugefügte Element immer als erstes vom Stack entfernt wird, man könnte es sich wie einen Plattenstapel vorstellen, bei dem nur die erste Platte, die zuletzt hinzugefügt wurde, zuerst entfernt werden kann. Ein Call Stack ist einfach nichts anderes als eine Stack-Datenstruktur, in der Aufgaben oder Code entsprechend ausgeführt werden.

Betrachten wir das folgende Beispiel;

Quelle – https://youtu.be/8aGhZQkoFbQ

Wenn Sie die Funktion printSquare() aufrufen, wird sie auf den Aufrufstapel geschoben, die Funktion printSquare() ruft die Funktion square() auf. Die square()-Funktion wird auf den Stapel geschoben und ruft auch die multiply()-Funktion auf. Die Multiplikationsfunktion wird auf den Stack geschoben. Da die multiply-Funktion zurückkehrt und das Letzte ist, was auf den Stack geschoben wurde, wird sie zuerst aufgelöst und aus dem Stack entfernt, gefolgt von der square()-Funktion und dann der printSquare()-Funktion.

Die Web-API

Hier wird Code ausgeführt, der nicht von der V8-Engine verarbeitet wird, um den Hauptausführungs-Thread nicht zu „blockieren“. Wenn der Call Stack auf eine Web-API-Funktion trifft, wird der Prozess sofort an die Web-API übergeben, wo er ausgeführt wird und den Call Stack freigibt, während seiner Ausführung andere Operationen durchzuführen.

Kehren wir zu unserem obigen setTimeout-Beispiel zurück.

Wenn wir den Code ausführen, wird die erste console.log-Zeile auf den Stack geschoben und wir erhalten unsere Ausgabe fast sofort. Wenn wir zum Timeout kommen, werden Timer vom Browser verarbeitet und sind kein Teil der Kernimplementierung von V8, sie werden geschoben stattdessen an die Web-API, wodurch der Stack freigegeben wird, damit er andere Operationen ausführen kann.

Während das Timeout noch läuft, fährt der Stack mit der nächsten Aktion fort und führt das letzte console.log aus, was erklärt, warum wir das vor der Timer-Ausgabe ausgegeben bekommen. Sobald der Timer abgelaufen ist, passiert etwas. Der console.log in then Timer taucht auf magische Weise wieder im Call Stack auf!

Wie?

Die Ereignisschleife

Bevor wir auf die Ereignisschleife eingehen, gehen wir zunächst die Funktion der Aufgabenwarteschlange durch.

Zurück zu unserem Timeout-Beispiel: Sobald die Web-API die Ausführung der Aufgabe beendet hat, überträgt sie sie nicht einfach automatisch zurück an den Call Stack. Es geht in die Aufgabenwarteschlange.

Eine Warteschlange ist eine Datenstruktur, die nach dem First-in-First-out-Prinzip funktioniert, sodass Aufgaben, die in die Warteschlange geschoben werden, in derselben Reihenfolge wieder herauskommen. Aufgaben, die von den Web-APIs ausgeführt wurden und in die Aufgabenwarteschlange verschoben werden, kehren dann zum Aufrufstapel zurück, um ihr Ergebnis auszudrucken.

Aber warte. WAS ZUM HECK IST DIE EREIGNISSCHLEIFE???

Quelle – https://youtu.be/8aGhZQkoFbQ

Die Ereignisschleife ist ein Prozess, der darauf wartet, dass der Call Stack gelöscht wird, bevor Rückrufe von der Aufgabenwarteschlange an den Call Stack verschoben werden. Sobald der Stapel gelöscht ist, wird die Ereignisschleife ausgelöst und die Aufgabenwarteschlange auf verfügbare Rückrufe überprüft. Wenn es welche gibt, schiebt es sie zum Call Stack, wartet darauf, dass der Call Stack wieder leer ist, und wiederholt denselben Vorgang.

Quelle – https://www.quora.com/How-does-an-event-loop-work/answer/Timothy-Maxwell

Das obige Diagramm zeigt den grundlegenden Arbeitsablauf zwischen der Ereignisschleife und der Aufgabenwarteschlange.

Fazit

Obwohl dies eine sehr grundlegende Einführung ist, gibt das Konzept der asynchronen Programmierung in JavaScript genügend Einblick, um klar zu verstehen, was unter der Haube vor sich geht und wie JavaScript mit nur einem einzigen Thread gleichzeitig und asynchron ausgeführt werden kann.

JavaScript ist immer auf Abruf verfügbar, und wenn Sie neugierig sind, würde ich Ihnen raten, dies zu überprüfen Udemy-Kurs.