JavaScript-Timer: alles was Sie wissen müssen

Hallo Kollegen. Es war einmal ein Artikel über Habré von John Rezig zu diesem Thema. 10 Jahre sind vergangen, und das Thema muss noch geklärt werden. Daher bieten wir Interessenten an, den Artikel von Samer Buna zu lesen, der nicht nur einen theoretischen Überblick über Timer in JavaScript (im Kontext von Node.js) gibt, sondern auch Aufgaben zu diesen.




Vor einigen Wochen habe ich die folgende Frage aus einem einzigen Interview getwittert:

„Wo ist der Quellcode für die Funktionen setTimeout und setInterval? Wo würdest du ihn suchen? Du kannst es nicht googeln :) "

*** Beantworte es selbst und lies dann weiter ***



Etwa die Hälfte der Antworten auf diesen Tweet war falsch. Nein, der Fall ist NICHT mit V8 (oder anderen VMs) verbunden !!! Funktionen wie setTimeout und setInterval , die stolz als JavaScript-JavaScript-Timer bezeichnet werden, sind nicht Teil einer ECMAScript-Spezifikation oder einer JavaScript-Engine-Implementierung. Timer-Funktionen werden auf Browserebene implementiert, sodass sich ihre Implementierung in verschiedenen Browsern unterscheidet. Timer sind auch nativ in der Node.js.-Laufzeit selbst implementiert.

In Browsern beziehen sich die Hauptzeitgeberfunktionen auf die Window , die auch einigen anderen Funktionen und Objekten zugeordnet ist. Diese Schnittstelle bietet globalen Zugriff auf alle Elemente im Hauptbereich von JavaScript. Aus diesem Grund kann die Funktion setTimeout direkt in der Browserkonsole ausgeführt werden.

In Node sind Timer Teil des global Objekts, das wie die Browser-Oberfläche von Window . Der Quellcode für die Timer in Node wird hier angezeigt.

Es mag jemandem scheinen, dass dies nur eine schlechte Frage aus dem Interview ist - was nützt es, das zu wissen ?! Ich als JavaScript-Entwickler denke so: Es wird davon ausgegangen, dass Sie dies wissen sollten, da das Gegenteil darauf hindeutet, dass Sie nicht ganz verstehen, wie V8 (und andere virtuelle Maschinen) mit Browsern und Node interagieren.

Schauen wir uns ein paar Beispiele an und lösen ein paar Timer-Aufgaben.

Mit dem Befehl node können Sie die Beispiele in diesem Artikel ausführen. Die meisten der hier diskutierten Beispiele wurden in meinem Kurs Erste Schritte mit Node.js zu Pluralsight vorgestellt.

Verzögerung der Funktionsausführung

Timer sind Funktionen höherer Ordnung, mit denen Sie die Ausführung anderer Funktionen verzögern oder wiederholen können (der Timer erhält eine solche Funktion als erstes Argument).

Hier ist ein Beispiel für eine verzögerte Ausführung:

 // example1.js setTimeout( () => { console.log('Hello after 4 seconds'); }, 4 * 1000 ); 

In diesem Beispiel wird die Begrüßungsnachricht bei Verwendung von setTimeout um 4 Sekunden verzögert. Das zweite Argument für setTimeout ist die Verzögerung (in ms). Ich multipliziere 4 mit 1000, um 4 Sekunden zu erhalten.

Das erste Argument für setTimeout ist eine Funktion, deren Ausführung verzögert wird.
Wenn Sie die Datei example1.js mit dem Befehl node ausführen, hält Node 4 Sekunden lang an und zeigt dann eine Willkommensnachricht an (gefolgt von einem Exit).

Bitte beachten Sie: Das erste Argument für setTimeout ist nur eine Funktionsreferenz . Es sollte keine eingebaute Funktion sein - wie zum Beispiel example1.js . Hier ist das gleiche Beispiel ohne Verwendung der integrierten Funktion:

 const func = () => { console.log('Hello after 4 seconds'); }; setTimeout(func, 4 * 1000); 

Argumente übergeben

Wenn die Funktion, für die setTimeout zum Verzögern verwendet wird, Argumente akzeptiert, können Sie die verbleibenden Argumente der Funktion setTimeout selbst (nach den 2, die wir bereits untersucht haben) verwenden, um die Werte der Argumente an die verzögerte Funktion zu übertragen.

 // : func(arg1, arg2, arg3, ...) //  : setTimeout(func, delay, arg1, arg2, arg3, ...) 

Hier ist ein Beispiel:

 // example2.js const rocks = who => { console.log(who + ' rocks'); }; setTimeout(rocks, 2 * 1000, 'Node.js'); 

Die obige rocks Funktion, die um 2 Sekunden verzögert ist, verwendet das who Argument, und beim Aufrufen von setTimeout wird der Wert "Node.js" als solches who Argument übergeben.

Wenn Sie example2.js mit dem Befehl node ausführen, wird nach 2 Sekunden der Ausdruck "Node.js rockt" angezeigt.

Timer Aufgabe 1

Basierend auf dem bereits untersuchten Material zu setTimeout werden die folgenden 2 Meldungen nach den entsprechenden Verzögerungen angezeigt.

  • Die Meldung „Hallo nach 4 Sekunden“ wird nach 4 Sekunden angezeigt.
  • Die Meldung „Hallo nach 8 Sekunden“ wird nach 8 Sekunden angezeigt.

Einschränkung

In Ihrer Lösung können Sie nur eine Funktion definieren, die integrierte Funktionen enthält. Dies bedeutet, dass viele setTimeout Aufrufe dieselbe Funktion verwenden müssen.

Lösung

So würde ich dieses Problem lösen:

 // solution1.js const theOneFunc = delay => { console.log('Hello after ' + delay + ' seconds'); }; setTimeout(theOneFunc, 4 * 1000, 4); setTimeout(theOneFunc, 8 * 1000, 8); 

Für mich empfängt theOneFunc das delay und verwendet den Wert dieses delay in der auf dem Bildschirm angezeigten Nachricht. Somit kann die Funktion unterschiedliche Meldungen anzeigen, je nachdem, welchen Verzögerungswert wir darüber informieren.

Dann habe ich theOneFunc in zwei setTimeout Aufrufen verwendet, wobei der erste Aufruf nach 4 Sekunden und der zweite nach 8 Sekunden ausgelöst wurde. Beide setTimeout Aufrufe erhalten auch ein setTimeout Argument, das das delay für theOneFunc .

Wenn Sie die Datei solution1.js mit dem Befehl node ausführen, werden die Anforderungen der Aufgabe angezeigt. Die erste Meldung wird nach 4 Sekunden und die zweite nach 8 Sekunden angezeigt.

Wiederholen Sie die Funktion

Aber was ist, wenn ich Sie gebeten habe, unbegrenzt alle 4 Sekunden eine Nachricht anzuzeigen?
Natürlich können Sie setTimeout in eine Schleife setTimeout , aber die Timer-API bietet auch die Funktion setInterval , mit der Sie die „ewige“ Ausführung jeder Operation programmieren können.

Hier ist ein Beispiel für setInterval :

 // example3.js setInterval( () => console.log('Hello every 3 seconds'), 3000 ); 

Dieser Code zeigt alle 3 Sekunden eine Meldung an. Wenn Sie example3.js mit dem Befehl node ausführen, gibt Node diesen Befehl aus, bis Sie das example3.js des Prozesses erzwingen (STRG + C).

Timer abbrechen

Da beim Aufrufen der Timer-Funktion eine Aktion zugewiesen wird, kann diese Aktion auch rückgängig gemacht werden, bevor sie ausgeführt wird.

Der Aufruf von setTimeout gibt eine Timer-ID zurück. Sie können diese Timer-ID beim Aufrufen von clearTimeout , um den Timer abzubrechen. Hier ist ein Beispiel:

 // example4.js const timerId = setTimeout( () => console.log('You will not see this one!'), 0 ); clearTimeout(timerId); 

Dieser einfache Timer sollte nach 0 ms ( timerId sofort) clearTimeout . Dies ist jedoch nicht der Fall, da wir den Wert von timerId und diesen Timer sofort durch Aufrufen von clearTimeout .

Wenn Sie example4.js mit dem Befehl node ausführen, example4.js Node nichts - der Vorgang wird einfach sofort beendet.

Übrigens bietet Node.js auch eine andere Möglichkeit, setTimeout mit einem Wert von 0 ms setTimeout . In der Node.js-Timer-API gibt es eine weitere Funktion namens setImmediate , die im Grunde dasselbe wie setTimeout mit einem Wert von 0 ms setTimeout In diesem Fall können Sie jedoch die Verzögerung weglassen:

 setImmediate( () => console.log('I am equivalent to setTimeout with 0 ms'), ); 

Die Funktion setImmediate wird setImmediate in allen Browsern unterstützt . Verwenden Sie es nicht im Client-Code.

Zusammen mit clearTimeout gibt es eine clearInterval Funktion, die dasselbe tut, jedoch mit setInerval Aufrufen, und es gibt auch einen clearImmediate Aufruf.

Timer Delay - eine Sache, die nicht garantiert ist

Haben Sie bemerkt, dass im vorherigen Beispiel beim Ausführen einer Operation mit setTimeout nach 0 ms diese Operation nicht sofort (nach setTimeout ) ausgeführt wird, sondern erst, nachdem der gesamte clearTimeout vollständig ausgeführt wurde (einschließlich des Aufrufs clearTimeout )?

Lassen Sie mich diesen Punkt anhand eines Beispiels verdeutlichen. Hier ist ein einfacher setTimeout Aufruf, der in einer halben Sekunde funktionieren sollte - aber das passiert nicht:

 // example5.js setTimeout( () => console.log('Hello after 0.5 seconds. MAYBE!'), 500, ); for (let i = 0; i < 1e10; i++) { //    } 

Unmittelbar nach dem Definieren des Timers in diesem Beispiel blockieren wir die Laufzeitumgebung synchron mit einer großen for Schleife. Der Wert von 1e10 ist 1 mit 10 Nullen, sodass der Zyklus 10 Milliarden Prozessorzyklen dauert (im Prinzip simuliert dies einen überlasteten Prozessor). Der Knoten kann nichts tun, bis diese Schleife abgeschlossen ist.

In der Praxis ist dies natürlich sehr schlecht, aber dieses Beispiel hilft zu verstehen, dass die setTimeout Verzögerung nicht garantiert ist, sondern der Mindestwert . Ein Wert von 500 ms bedeutet, dass die Verzögerung mindestens 500 ms beträgt. Tatsächlich dauert es viel länger, bis das Skript die Begrüßungszeile auf dem Bildschirm anzeigt. Zunächst muss er warten, bis der Sperrzyklus abgeschlossen ist.

Timer Problem # 2

Schreiben Sie ein Skript, in dem die Meldung „Hallo Welt“ einmal pro Sekunde, jedoch nur fünfmal angezeigt wird. Nach 5 Iterationen sollte das Skript die Meldung "Fertig" anzeigen. Danach wird der Knotenprozess abgeschlossen.

Einschränkung : Wenn Sie dieses Problem lösen, können Sie setTimeout nicht aufrufen.

Hinweis : brauche einen Zähler.

Lösung

So würde ich dieses Problem lösen:

 let counter = 0; const intervalId = setInterval(() => { console.log('Hello World'); counter += 1; if (counter === 5) { console.log('Done'); clearInterval(intervalId); } }, 1000); 

Ich setze 0 als Anfangswert des counter und setInterval dann setInterval , das seine ID annimmt.

Eine verzögerte Funktion zeigt eine Meldung an und erhöht den Zähler jedes Mal um eins. Innerhalb der verzögerten Funktion haben wir eine if-Anweisung, die prüft, ob bereits 5 Iterationen bestanden wurden. Nach 5 Iterationen zeigt das Programm „Fertig“ an und löscht den Intervallwert mithilfe der erfassten intervalId Konstante. Die Intervallverzögerung beträgt 1000 ms.

Wer nennt genau verzögerte Funktionen?

Bei Verwendung von JavaScript this in einer regulären Funktion, wie zum Beispiel:

 function whoCalledMe() { console.log('Caller is', this); } 

Der Wert im this mit dem Anrufer überein. Wenn Sie die obige Funktion innerhalb des Knotens REPL definieren, wird sie vom global Objekt aufgerufen. Wenn Sie eine Funktion in der Browserkonsole definieren, wird sie vom window aufgerufen.

Definieren wir eine Funktion als Eigenschaft eines Objekts, um es etwas klarer zu machen:

 const obj = { id: '42', whoCalledMe() { console.log('Caller is', this); } }; //     : obj.whoCallMe 

Wenn wir nun bei der Arbeit mit der Funktion obj den Link direkt dazu verwenden, fungiert das Objekt obj (identifiziert durch seine id ) als Aufrufer:



Die Frage ist nun: Wer wird der Anrufer sein, wenn Sie den Link an obj.whoCallMe an setTimetout ?

 //       ?? setTimeout(obj.whoCalledMe, 0); 

Wer ist in diesem Fall der Anrufer?

Die Antwort hängt davon ab, wo die Timer-Funktion ausgeführt wird. In diesem Fall ist die Abhängigkeit davon, wer der Anrufer ist, einfach nicht akzeptabel. Sie verlieren die Kontrolle über den Aufrufer, da dies von der Implementierung des Timers abhängt, der in diesem Fall Ihre Funktion aufruft. Wenn Sie diesen Code in einem Node REPL testen, ist das Timeout Objekt der Aufrufer:



Bitte beachten Sie: Dies ist nur wichtig, wenn das JavaScript this in regulären Funktionen verwendet wird. Bei Verwendung der Pfeilfunktionen sollte der Anrufer Sie überhaupt nicht stören.

Timer Problem # 3

Schreiben Sie ein Skript, das kontinuierlich eine Nachricht "Hallo Welt" mit unterschiedlichen Verzögerungen ausgibt. Beginnen Sie mit einer Verzögerung von einer Sekunde und erhöhen Sie diese bei jeder Iteration um eine Sekunde. Bei der zweiten Iteration beträgt die Verzögerung 2 Sekunden. Am dritten - drei und so weiter.

Fügen Sie der angezeigten Nachricht eine Verzögerung hinzu. Sie sollten so etwas bekommen:

Hello World. 1
Hello World. 2
Hello World. 3
...


Einschränkungen : Variablen können nur mit const definiert werden. Let oder var verwenden ist nicht.

Lösung

Da die Dauer der Verzögerung in dieser Aufgabe eine Variable ist, können Sie setInterval hier nicht verwenden, aber Sie können die Intervallausführung mithilfe von setTimeout innerhalb eines rekursiven Aufrufs manuell konfigurieren. Die erste mit setTimeout ausgeführte Funktion erstellt den nächsten Timer usw.

Da Sie let / var nicht verwenden können, können wir außerdem keinen Zähler haben, um die Verzögerung für jeden rekursiven Aufruf zu erhöhen. Stattdessen können Sie die Argumente einer rekursiven Funktion verwenden, um während eines rekursiven Aufrufs ein Inkrement durchzuführen.

So lösen Sie dieses Problem:

 const greeting = delay => setTimeout(() => { console.log('Hello World. ' + delay); greeting(delay + 1); }, delay * 1000); greeting(1); 

Timer Aufgabe 4

Schreiben Sie ein Skript, das die Nachricht „Hello World“ mit der gleichen Verzögerungsstruktur wie in Aufgabe 3 anzeigt, diesmal jedoch in Gruppen von 5 Nachrichten, und die Gruppe hat ein Hauptverzögerungsintervall. Für die erste Gruppe von 5 Nachrichten wählen wir die anfängliche Verzögerung von 100 ms, für die nächsten - 200 ms, für die dritten - 300 ms und so weiter.

So sollte dieses Skript funktionieren:

  • Bei 100 ms zeigt das Skript zum ersten Mal „Hallo Welt“ an, und zwar fünfmal mit einem Intervall von 100 ms. Die erste Meldung erscheint nach 100 ms, die zweite nach 200 ms usw.
  • Nach den ersten 5 Nachrichten sollte das Skript die Hauptverzögerung um 200 ms erhöhen. Somit wird die 6. Nachricht nach 500 ms + 200 ms (700 ms), die 7. - 900 ms, die 8. Nachricht - nach 1100 ms usw. angezeigt.
  • Nach 10 Nachrichten sollte das Skript das Hauptverzögerungsintervall um 300 ms erhöhen. Die 11. Meldung sollte nach 500 ms + 1000 ms + 300 ms (18000 ms) angezeigt werden. Die 12. Meldung sollte nach 2100 ms usw. angezeigt werden.

Nach diesem Prinzip sollte das Programm auf unbestimmte Zeit funktionieren.

Fügen Sie der angezeigten Nachricht eine Verzögerung hinzu. Sie sollten so etwas bekommen (kein Kommentar):

Hello World. 100 // 100
Hello World. 100 // 200
Hello World. 100 // 300
Hello World. 100 // 400
Hello World. 100 // 500
Hello World. 200 // 700
Hello World. 200 // 900
Hello World. 200 // 1100
...


Einschränkungen : Sie können nur Aufrufe von setInterval (und nicht setTimeout ) und nur ONE if .

Lösung

Da wir nur mit setInterval Aufrufen arbeiten können, müssen wir hier die Rekursion verwenden und auch die Verzögerung des nächsten setInterval Aufrufs setInterval . Außerdem benötigen wir die if , um dies erst nach 5 Aufrufen dieser rekursiven Funktion zu erreichen.

Hier ist eine mögliche Lösung:

 let lastIntervalId, counter = 5; const greeting = delay => { if (counter === 5) { clearInterval(lastIntervalId); lastIntervalId = setInterval(() => { console.log('Hello World. ', delay); greeting(delay + 100); }, delay); counter = 0; } counter += 1; }; greeting(100); 

Vielen Dank an alle, die es gelesen haben.

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


All Articles