Node.js Guide, Teil 6: Ereignisschleife, Call Stack, Timer

Heute, im sechsten Teil der Übersetzung des Node.js-Handbuchs, werden wir über die Ereignisschleife, den Aufrufstapel, die Funktion process.nextTick() und Timer sprechen. Das Verständnis dieser und anderer Node.js-Mechanismen ist einer der Eckpfeiler einer erfolgreichen Anwendungsentwicklung für diese Plattform.




Ereignisschleife


Wenn Sie verstehen möchten, wie JavaScript-Code ausgeführt wird, ist die Ereignisschleife eines der wichtigsten Konzepte, die Sie verstehen müssen. Hier werden wir darüber sprechen, wie JavaScript im Single-Threaded-Modus funktioniert und wie asynchrone Funktionen behandelt werden.

Ich habe JavaScript seit vielen Jahren entwickelt, aber ich kann nicht sagen, dass ich vollständig verstanden habe, wie alles sozusagen "unter der Haube" funktioniert. Dem Programmierer sind möglicherweise die Feinheiten des Geräts der internen Subsysteme der Umgebung, in der er arbeitet, nicht bekannt. Aber es ist normalerweise nützlich, zumindest eine allgemeine Vorstellung von solchen Dingen zu haben.

Der von Ihnen geschriebene JavaScript-Code wird im Single-Thread-Modus ausgeführt. Zu einem bestimmten Zeitpunkt wird nur eine Aktion ausgeführt. Diese Einschränkung ist in der Tat sehr nützlich. Dies vereinfacht die Arbeitsweise von Programmen erheblich und macht es für Programmierer unnötig, Probleme zu lösen, die für Multithread-Umgebungen spezifisch sind.

Tatsächlich muss ein JS-Programmierer nur genau darauf achten, welche Aktionen sein Code ausführt, und versuchen, Situationen zu vermeiden, die zum Blockieren des Hauptthreads führen. Zum Beispiel - Netzwerkanrufe im synchronen Modus und in endlosen Zyklen .

In der Regel haben Browser in jedem geöffneten Tab eine eigene Ereignisschleife. Auf diese Weise können Sie den Code jeder Seite in einer isolierten Umgebung ausführen und Situationen vermeiden, in denen eine bestimmte Seite, in deren Code eine Endlosschleife vorhanden ist oder umfangreiche Berechnungen durchgeführt werden, den gesamten Browser "anhalten" kann. Der Browser unterstützt die Arbeit vieler gleichzeitig vorhandener Ereignisschleifen, die beispielsweise zum Verarbeiten von Aufrufen an verschiedene APIs verwendet werden. Darüber hinaus wird eine proprietäre Ereignisschleife zur Unterstützung von Web-Workern verwendet .

Das Wichtigste, an das sich ein JavaScript-Programmierer ständig erinnern muss, ist, dass sein Code eine eigene Ereignisschleife verwendet. Daher muss der Code so geschrieben werden, dass diese Ereignisschleife nicht blockiert wird.

Ereignisschleifensperre


Jeder JavaScript-Code, dessen Ausführung zu lange dauert, dh Code, der die Ereignisschleife nicht zu lange kontrolliert, blockiert die Ausführung eines anderen Seitencodes. Dies führt sogar dazu, dass die Verarbeitung von Benutzeroberflächenereignissen blockiert wird. Dies spiegelt sich darin wider, dass der Benutzer nicht mit den Seitenelementen interagieren und normal damit arbeiten kann, z. B. beim Scrollen.

Fast alle grundlegenden JavaScript-E / A-Mechanismen sind nicht blockierend. Dies gilt sowohl für den Browser als auch für Node.js. Unter solchen Mechanismen können wir beispielsweise die Tools zum Ausführen von Netzwerkanforderungen erwähnen, die sowohl in Client- als auch in Serverumgebungen verwendet werden, sowie Tools zum Arbeiten mit Node.js-Dateien. Es gibt synchrone Methoden zum Ausführen solcher Operationen, die jedoch nur in besonderen Fällen verwendet werden. Aus diesem Grund sind traditionelle Rückrufe und neuere Mechanismen - Versprechen und das Konstrukt async / await - in JavaScript von großer Bedeutung.

Stapel aufrufen


Der JavaScript Call Stack basiert auf dem LIFO-Prinzip (Last In, First Out - Last In, First Out). Die Ereignisschleife überprüft ständig den Aufrufstapel, um festzustellen, ob eine Funktion ausgeführt werden muss. Wenn beim Ausführen des Codes eine Funktion darin aufgerufen wird, werden Informationen dazu zum Aufrufstapel hinzugefügt und diese Funktion ausgeführt.

Wenn Sie sich schon vorher nicht für das Konzept eines „Aufrufstapels“ interessiert haben und dann auf Fehlermeldungen gestoßen sind, die eine Stapelverfolgung enthalten, stellen Sie sich bereits vor, wie es aussieht. Hier sieht es zum Beispiel in einem Browser so aus.


Browser-Fehlermeldung

Wenn ein Fehler auftritt, meldet der Browser die Reihenfolge der Aufrufe von Funktionen, deren Informationen im Aufrufstapel gespeichert sind. Auf diese Weise können Sie die Fehlerquelle ermitteln und nachvollziehen, welche Aufrufe zu welchen Funktionen zur Situation geführt haben.

Nachdem wir nun allgemein über die Ereignisschleife und den Aufrufstapel gesprochen haben, betrachten wir ein Beispiel, das die Ausführung eines Codefragments veranschaulicht und wie dieser Prozess aus der Sicht der Ereignisschleife und des Aufrufstapels aussieht.

Ereignisschleife und Call Stack


Hier ist der Code, mit dem wir experimentieren werden:

 const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') bar() baz() } foo() 

Wenn dieser Code ausgeführt wird, gelangt Folgendes zur Konsole:

 foo bar baz 

Ein solches Ergebnis wird durchaus erwartet. Wenn dieser Code ausgeführt wird, wird nämlich zuerst die Funktion foo() aufgerufen. Innerhalb dieser Funktion rufen wir zuerst die Funktion bar() und dann die Funktion baz() . Gleichzeitig erfährt der Aufrufstapel während der Ausführung dieses Codes die in der folgenden Abbildung gezeigten Änderungen.


Ändern des Status des Aufrufstapels beim Ausführen des Codes

Die Ereignisschleife prüft bei jeder Iteration, ob sich etwas im Aufrufstapel befindet, und wenn ja, bis der Aufrufstapel leer ist.


Ereignisschleifeniterationen

Eine Funktion in die Warteschlange stellen


Das obige Beispiel sieht ganz normal aus, es gibt nichts Besonderes: JavaScript findet den Code, der ausgeführt werden muss, und führt ihn der Reihe nach aus. Wir werden darüber sprechen, wie die Funktionsausführung verschoben werden kann, bis der Aufrufstapel gelöscht ist. Zu diesem Zweck wird die folgende Konstruktion verwendet:

 setTimeout(() => {}), 0) 

Sie können die an die Funktion setTimeout() Funktion ausführen, nachdem alle anderen im Programmcode aufgerufenen Funktionen ausgeführt wurden.

Betrachten Sie ein Beispiel:

 const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) baz() } foo() 

Was dieser Code druckt, kann unerwartet erscheinen:

 foo baz bar 

Wenn wir dieses Beispiel ausführen, wird zuerst die Funktion foo() aufgerufen. Darin rufen wir setTimeout() und übergeben diese Funktion als erstes Argument bar . Indem wir 0 als zweites Argument übergeben, informieren wir das System, dass diese Funktion so schnell wie möglich ausgeführt werden soll. Dann rufen wir die Funktion baz() .

So sieht der Aufrufstapel jetzt aus.


Ändern des Status des Aufrufstapels bei der Ausführung des untersuchten Codes

Hier ist die Reihenfolge, in der die Funktionen in unserem Programm jetzt ausgeführt werden.


Ereignisschleifeniterationen

Warum passiert das so?

Ereigniswarteschlange


Wenn die Funktion setTimeout() aufgerufen wird, startet der Browser oder die Node.js-Plattform einen Timer. Nachdem der Timer funktioniert (in unserem Fall geschieht dies sofort, da wir ihn auf 0 gesetzt haben), wird die an setTimeout() Rückruffunktion in die Ereigniswarteschlange gestellt.

Die Ereigniswarteschlange enthält im Browser vom Benutzer initiierte Ereignisse - Ereignisse, die durch Mausklicks auf Seitenelemente verursacht werden, Ereignisse, die ausgelöst werden, wenn Daten über die Tastatur eingegeben werden. DOM- onload wie onload , Funktionen, die beim Empfang von Antworten auf asynchrone Anforderungen zum Laden von Daten aufgerufen werden, sind sofort onload . Hier warten sie darauf, dass sie an die Reihe kommen.

Die Ereignisschleife gibt dem, was sich auf dem Aufrufstapel befindet, Priorität. Zuerst macht es alles, was es auf dem Stapel findet, und nachdem der Stapel leer ist, verarbeitet es, was sich in der Ereigniswarteschlange befindet.

Wir müssen nicht warten, bis eine Funktion wie setTimeout() , da ähnliche Funktionen vom Browser bereitgestellt werden und sie ihre eigenen Streams verwenden. setTimeout() Sie beispielsweise den Timer mit der Funktion setTimeout() auf 2 Sekunden setTimeout() , sollten Sie nach dem Stoppen der Ausführung eines anderen Codes nicht auf diese 2 Sekunden warten, da der Timer außerhalb Ihres Codes arbeitet.

ES6-Jobwarteschlange


ECMAScript 2015 (ES6) führte das Konzept der Job Queue ein, das von Versprechungen verwendet wird (sie erschienen auch in ES6). Dank der Jobwarteschlange kann das Ergebnis der Ausführung der asynchronen Funktion so schnell wie möglich verwendet werden, ohne dass auf das Löschen des Aufrufstapels gewartet werden muss.

Wenn ein Versprechen vor dem Ende der aktuellen Funktion aufgelöst wird, wird der entsprechende Code unmittelbar nach Abschluss der aktuellen Funktion ausgeführt.

Ich habe eine interessante Analogie für das gefunden, worüber wir sprechen. Dies kann mit einer Achterbahn in einem Vergnügungspark verglichen werden. Nachdem Sie den Hügel gefahren sind und es erneut tun möchten, nehmen Sie ein Ticket und steigen in die Warteschlange ein. So funktioniert die Ereigniswarteschlange. Die Jobwarteschlange sieht jedoch anders aus. Dieses Konzept ähnelt einem ermäßigten Ticket, mit dem Sie das Recht haben, die nächste Reise unmittelbar nach Beendigung der vorherigen zu unternehmen.

Betrachten Sie das folgende Beispiel:

 const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) new Promise((resolve, reject) =>   resolve('should be right after baz, before bar') ).then(resolve => console.log(resolve)) baz() } foo() 

Folgendes wird nach der Ausführung ausgegeben:

 foo baz should be right after baz, before bar bar 

Was Sie hier sehen können, zeigt einen gravierenden Unterschied zwischen Versprechungen (und dem darauf basierenden asynchronen / wartenden Konstrukt) und traditionellen asynchronen Funktionen, deren Ausführung mithilfe von setTimeout() oder anderen APIs der verwendeten Plattform organisiert wird.

process.nextTick ()


Die Methode process.nextTick() interagiert auf besondere Weise mit der Ereignisschleife. Ein Tick ist ein einzelner vollständiger Zyklus von Ereignissen. Wenn wir die Funktion an die process.nextTick() -Methode übergeben, informieren wir das System, dass diese Funktion aufgerufen werden muss, nachdem die aktuelle Iteration der Ereignisschleife abgeschlossen ist, bevor die nächste beginnt. Die Verwendung dieser Methode sieht folgendermaßen aus:

 process.nextTick(() => { // -  }) 

Angenommen, eine Ereignisschleife führt gerade Code für die aktuelle Funktion aus. Wenn dieser Vorgang abgeschlossen ist, führt die JavaScript-Engine alle Funktionen aus, die während des vorherigen Vorgangs an process.nextTick() wurden. Mit diesem Mechanismus möchten wir sicherstellen, dass eine bestimmte Funktion asynchron (nach der aktuellen Funktion) ausgeführt wird, jedoch so schnell wie möglich, ohne sie in die Warteschlange zu stellen.

Wenn Sie beispielsweise das setTimeout(() => {}, 0) verwenden, wird die Funktion bei der nächsten Iteration der Ereignisschleife ausgeführt, setTimeout(() => {}, 0) viel später als wenn process.nextTick() in derselben Situation verwendet wird. Diese Methode sollte verwendet werden, wenn die Ausführung von Code zu Beginn der nächsten Iteration der Ereignisschleife sichergestellt werden muss.

setImmediate ()


Eine weitere von Node.js bereitgestellte Funktion für die asynchrone Codeausführung ist setImmediate() . So verwenden Sie es:

 setImmediate(() => { //   }) 

Die an setImmediate() Rückruffunktion wird bei der nächsten Iteration der Ereignisschleife ausgeführt.

Wie unterscheidet sich setImmediate() von setTimeout(() => {}, 0) ( setTimeout(() => {}, 0) von einem Timer, der so schnell wie möglich funktionieren sollte) und von process.nextTick() ?

Die an process.nextTick() wird ausgeführt, nachdem die aktuelle Iteration der Ereignisschleife abgeschlossen wurde. Das heißt, eine solche Funktion wird immer vor der Funktion ausgeführt, deren Ausführung mit setTimeout() oder setImmediate() .

Das Aufrufen der Funktion setTimeout() mit einer festgelegten Verzögerung von 0 ms ist dem Aufrufen von setImmediate() sehr ähnlich. Die Reihenfolge der Ausführung der an sie übertragenen Funktionen hängt von verschiedenen Faktoren ab. In beiden Fällen werden jedoch bei der nächsten Iteration der Ereignisschleife Rückrufe aufgerufen.

Timer


Wir haben bereits über die Funktion setTimeout() , mit der Sie Aufrufe an die an sie übergebenen Rückrufe planen können. Nehmen wir uns etwas Zeit, um die Funktionen setInterval() zu beschreiben und eine andere ähnliche Funktion, setInterval() , zu betrachten. In Node.js sind Funktionen zum Arbeiten mit Timern im Timer- Modul enthalten. Sie können sie jedoch verwenden, ohne dieses Modul im Code zu verbinden, da sie global sind.

▍ Funktion setTimeout ()


Denken Sie daran, dass beim Aufrufen der Funktion setTimeout() ein Rückruf und die Uhrzeit in Millisekunden empfangen werden, nach der der Rückruf aufgerufen wird. Betrachten Sie ein Beispiel:

 setTimeout(() => { //   2  }, 2000) setTimeout(() => { //   50  }, 50) 

Hier übergeben wir setTimeout() neue Funktion, die sofort beschrieben wird. Hier können wir die vorhandene Funktion verwenden, indem wir setTimeout() ihren Namen und eine Reihe von Parametern übergeben, um sie auszuführen. Es sieht so aus:

 const myFunction = (firstParam, secondParam) => { //   } //   2  setTimeout(myFunction, 2000, firstParam, secondParam) 

Die Funktion setTimeout() gibt eine Timer- setTimeout() zurück. Normalerweise wird es nicht verwendet, aber Sie können es speichern und gegebenenfalls den Timer löschen, wenn der geplante Rückruf nicht mehr benötigt wird:

 const id = setTimeout(() => { //      2  }, 2000) //  ,       clearTimeout(id) 

▍ Keine Verzögerung


In den vorherigen Abschnitten haben wir setTimeout() und es als die Zeit übergeben, nach der der Rückruf 0 aufgerufen werden muss. Dies bedeutete, dass der Rückruf so schnell wie möglich aufgerufen wurde, jedoch nach Abschluss der aktuellen Funktion:

 setTimeout(() => { console.log('after ') }, 0) console.log(' before ') 

Ein solcher Code gibt Folgendes aus:

 before after 

Diese Technik ist besonders nützlich in Situationen, in denen ich bei der Ausführung schwerer Rechenaufgaben den Hauptthread nicht blockieren möchte, um die Ausführung anderer Funktionen zu ermöglichen und diese Aufgaben in mehrere Stufen zu unterteilen, die als setTimeout() -Aufrufe ausgeführt werden.

Wenn wir uns an die obige Funktion setImmediate() erinnern, ist sie in Node.js Standard, was nicht über Browser gesagt werden kann (sie ist in IE und Edge implementiert , aber nicht in anderen).

▍ Funktion setInterval ()


Die Funktion setInterval() ähnelt setTimeout() , es gibt jedoch Unterschiede zwischen ihnen. Anstatt den an ihn übergebenen Rückruf einmal setInterval() , setInterval() diesen Rückruf regelmäßig mit dem angegebenen Intervall auf. Dies wird im Idealfall so lange fortgesetzt, bis der Programmierer diesen Prozess explizit stoppt. So verwenden Sie diese Funktion:

 setInterval(() => { //   2  }, 2000) 

Ein an die oben gezeigte Funktion übergebener Rückruf wird alle 2 Sekunden aufgerufen. Um die Möglichkeit zu bieten, diesen Prozess zu stoppen, müssen Sie die von setInterval() Timer- setInterval() und den Befehl clearInterval() :

 const id = setInterval(() => { //   2  }, 2000) clearInterval(id) 

Eine übliche Technik besteht darin, clearInterval() innerhalb des an setInterval() Rückrufs setInterval() wenn eine bestimmte Bedingung erfüllt ist. Der folgende Code wird beispielsweise regelmäßig ausgeführt, bis die App.somethingIWait Eigenschaft auf " arrived :

 const interval = setInterval(function() { if (App.somethingIWait === 'arrived') {   clearInterval(interval)   //    -  ,   -    } }, 100) 

▍ Rekursive Einstellung setTimeout ()


Die Funktion setInterval() ruft den an sie übergebenen Rückruf alle n Millisekunden auf, ohne sich Gedanken darüber zu machen, ob dieser Rückruf nach dem vorherigen Aufruf abgeschlossen wurde.

Wenn jeder Aufruf dieses Rückrufs immer dieselbe Zeit von weniger als n , treten hier keine Probleme auf.


Periodisch aufgerufene Rückruf, deren Ausführungssitzung dieselbe Zeit in Anspruch nimmt und in das Intervall zwischen den Aufrufen fällt

Möglicherweise dauert es eine andere Zeit, um einen Rückruf abzuschließen, der immer noch kleiner als n . Wenn wir zum Beispiel über die Durchführung bestimmter Netzwerkoperationen sprechen, ist diese Situation durchaus zu erwarten.


Periodisch aufgerufene Rückruf, deren Ausführungssitzung eine andere Zeit in Anspruch nimmt und zwischen den Aufrufen liegt

Bei Verwendung von setInterval() kann es vorkommen, dass der Rückruf länger als n dauert, was dazu führt, dass der nächste Aufruf abgeschlossen wird, bevor der vorherige abgeschlossen ist.


Rückruf in regelmäßigen Abständen, wobei jede Sitzung eine andere Zeit benötigt, was manchmal nicht in das Intervall zwischen den Anrufen passt

Um diese Situation zu vermeiden, können Sie die rekursive Timer-Einstellungstechnik mit setTimeout() . Der Punkt ist, dass der nächste Rückruf nach Abschluss des vorherigen Anrufs geplant ist:

 const myFunction = () => { //    setTimeout(myFunction, 1000) } setTimeout( myFunction() }, 1000) 

Mit diesem Ansatz kann das folgende Szenario implementiert werden:


Ein rekursiver Aufruf von setTimeout (), um die Ausführung von Rückrufen zu planen

Zusammenfassung


Heute haben wir über die internen Mechanismen von Node.js gesprochen, wie z. B. die Ereignisschleife, den Aufrufstapel, und die Arbeit mit Timern besprochen, mit denen Sie die Codeausführung planen können. Das nächste Mal werden wir uns mit dem Thema asynchrone Programmierung befassen.

Liebe Leser! Sind Sie auf Situationen gestoßen, in denen Sie process.nextTick () verwenden mussten?

Source: https://habr.com/ru/post/de424553/


All Articles