Möglicherweise entscheiden wir uns, eine Funktion nicht jetzt, sondern zu einem bestimmten Zeitpunkt später auszuführen. Das nennt man „Anruf planen“.
Dafür gibt es zwei Methoden:
setTimeout
ermöglicht es uns, eine Funktion einmal nach Ablauf des Zeitintervalls auszuführen.
setInterval
können wir eine Funktion wiederholt ausführen, beginnend nach dem Zeitintervall und dann kontinuierlich in diesem Intervall wiederholen.
Diese Methoden sind nicht Teil der JavaScript-Spezifikation. Die meisten Umgebungen verfügen jedoch über einen internen Scheduler und stellen diese Methoden bereit. Insbesondere werden sie in allen Browsern und Node.js unterstützt.
Die Syntax:
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
Parameter:
func|code
Funktion oder eine Codezeichenfolge, die ausgeführt werden soll. Normalerweise ist das eine Funktion. Aus historischen Gründen kann eine Codezeichenfolge übergeben werden, dies wird jedoch nicht empfohlen.
delay
Die Verzögerung vor der Ausführung in Millisekunden (1000 ms = 1 Sekunde), standardmäßig 0.
arg1
, arg2
…
Argumente für die Funktion
Dieser Code ruft beispielsweise sayHi()
nach einer Sekunde auf:
Funktion sayHi() { alarm('Hallo'); } setTimeout(sayHi, 1000);
Mit Argumenten:
Funktion sayHi(phrase, who) { alarm( Phrase + ', ' + who ); } setTimeout(sayHi, 1000, „Hallo“, „John“); // Hallo, John
Wenn das erste Argument ein String ist, erstellt JavaScript daraus eine Funktion.
Das wird also auch funktionieren:
setTimeout("alert('Hallo')", 1000);
Die Verwendung von Zeichenfolgen wird jedoch nicht empfohlen. Verwenden Sie stattdessen Pfeilfunktionen wie folgt:
setTimeout(() => alarm('Hallo'), 1000);
Übergeben Sie eine Funktion, aber führen Sie sie nicht aus
Anfänger machen manchmal den Fehler, Klammern ()
nach der Funktion hinzuzufügen:
// falsch! setTimeout(sayHi(), 1000);
Das funktioniert nicht, da setTimeout
einen Verweis auf eine Funktion erwartet. Und hier führt sayHi()
die Funktion aus und das Ergebnis ihrer Ausführung wird an setTimeout
übergeben. In unserem Fall ist das Ergebnis von sayHi()
undefined
(die Funktion gibt nichts zurück), daher ist nichts geplant.
Ein Aufruf von setTimeout
gibt eine „Timer-ID“ timerId
zurück, mit der wir die Ausführung abbrechen können.
Die Syntax zum Abbrechen:
let timerId = setTimeout(...); clearTimeout(timerId);
Im folgenden Code planen wir die Funktion und brechen sie dann ab (haben es uns anders überlegt). Infolgedessen passiert nichts:
let timerId = setTimeout(() => alarm("nie passiert"), 1000); alarm(timerId); // Timer-ID clearTimeout(timerId); alarm(timerId); // gleicher Bezeichner (wird nach dem Abbruch nicht null)
Wie wir aus alert
ersehen können, ist die Timer-ID in einem Browser eine Zahl. In anderen Umgebungen kann dies etwas anderes sein. Node.js gibt beispielsweise ein Timer-Objekt mit zusätzlichen Methoden zurück.
Auch hier gibt es keine universelle Spezifikation für diese Methoden, das ist also in Ordnung.
Für Browser werden Timer im Abschnitt „Timer“ von HTML Living Standard beschrieben.
Die setInterval
-Methode hat dieselbe Syntax wie setTimeout
:
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
Alle Argumente haben die gleiche Bedeutung. Aber im Gegensatz zu setTimeout
führt es die Funktion nicht nur einmal aus, sondern regelmäßig nach dem angegebenen Zeitintervall.
Um weitere Aufrufe zu stoppen, sollten wir clearInterval(timerId)
aufrufen.
Im folgenden Beispiel wird die Meldung alle 2 Sekunden angezeigt. Nach 5 Sekunden wird die Ausgabe gestoppt:
// im Abstand von 2 Sekunden wiederholen let timerId = setInterval(() => alarm('tick'), 2000); // nach 5 Sekunden stoppen setTimeout(() => { clearInterval(timerId); alarm('stop'); }, 5000);
Die Zeit läuft weiter, während alert
angezeigt wird
In den meisten Browsern, einschließlich Chrome und Firefox, „tickt“ der interne Timer weiter und zeigt gleichzeitig alert/confirm/prompt
an.
Wenn Sie also den obigen Code ausführen und das alert
einige Zeit lang nicht schließen, wird die nächste alert
sofort angezeigt, während Sie dies tun. Das tatsächliche Intervall zwischen den Warnungen beträgt weniger als 2 Sekunden.
Es gibt zwei Möglichkeiten, etwas regelmäßig durchzuführen.
Eine davon ist setInterval
. Das andere ist ein verschachteltes setTimeout
, etwa so:
/** anstatt: let timerId = setInterval(() => alarm('tick'), 2000); */ let timerId = setTimeout(function tick() { alarm('tick'); timerId = setTimeout(tick, 2000); // (*) }, 2000);
Das obige setTimeout
plant den nächsten Anruf direkt am Ende des aktuellen (*)
.
Das verschachtelte setTimeout
ist eine flexiblere Methode als setInterval
. Auf diese Weise kann der nächste Anruf je nach den Ergebnissen des aktuellen anders geplant werden.
Zum Beispiel müssen wir einen Dienst schreiben, der alle 5 Sekunden eine Anfrage an den Server sendet und nach Daten fragt. Falls der Server jedoch überlastet ist, sollte er das Intervall auf 10, 20, 40 Sekunden erhöhen ...
Hier ist der Pseudocode:
let Verzögerung = 5000; let timerId = setTimeout(function request() { ...Anfrage senden... if (Anfrage aufgrund von Serverüberlastung fehlgeschlagen) { // das Intervall bis zum nächsten Lauf erhöhen Verzögerung *= 2; } timerId = setTimeout(Anfrage, Verzögerung); }, Verzögerung);
Und wenn die von uns geplanten Funktionen CPU-hungrig sind, können wir die für die Ausführung benötigte Zeit messen und den nächsten Aufruf früher oder später planen.
Mit setTimeout
kann die Verzögerung zwischen den Ausführungen genauer eingestellt werden als setInterval
.
Vergleichen wir zwei Codefragmente. Der erste verwendet setInterval
:
sei i = 1; setInterval(function() { func(i++); }, 100);
Der zweite verwendet verschachteltes setTimeout
:
sei i = 1; setTimeout(function run() { func(i++); setTimeout(run, 100); }, 100);
Für setInterval
führt der interne Scheduler alle 100 ms func(i++)
aus:
Hast du es bemerkt?
Die tatsächliche Verzögerung zwischen func
für setInterval
ist geringer als im Code!
Das ist normal, denn die für die Ausführung von func
benötigte Zeit „verbraucht“ einen Teil des Intervalls.
Es ist möglich, dass die Ausführung von func
länger dauert als erwartet und mehr als 100 ms dauert.
In diesem Fall wartet die Engine auf den Abschluss von func
, überprüft dann den Scheduler und führt ihn sofort erneut aus, wenn die Zeit abgelaufen ist.
Wenn die Funktion im Randfall immer länger als delay
ms ausgeführt wird, erfolgen die Aufrufe überhaupt ohne Pause.
Und hier ist das Bild für das verschachtelte setTimeout
:
Das verschachtelte setTimeout
garantiert die feste Verzögerung (hier 100ms).
Das liegt daran, dass ein neuer Anruf am Ende des vorherigen geplant ist.
Garbage Collection und setInterval/setTimeout-Rückruf
Wenn eine Funktion in setInterval/setTimeout
übergeben wird, wird eine interne Referenz darauf erstellt und im Scheduler gespeichert. Dadurch wird verhindert, dass die Funktion durch Garbage Collection erfasst wird, auch wenn keine anderen Verweise darauf vorhanden sind.
// Die Funktion bleibt im Speicher, bis der Scheduler sie aufruft setTimeout(function() {...}, 100);
Für setInterval
bleibt die Funktion im Speicher, bis clearInterval
aufgerufen wird.
Es gibt einen Nebeneffekt. Eine Funktion referenziert die äußere lexikalische Umgebung, sodass während sie lebt, auch äußere Variablen leben. Sie beanspruchen möglicherweise viel mehr Speicher als die Funktion selbst. Wenn wir die geplante Funktion nicht mehr benötigen, ist es besser, sie abzubrechen, auch wenn sie sehr klein ist.
Es gibt einen speziellen Anwendungsfall: setTimeout(func, 0)
oder einfach setTimeout(func)
.
Dadurch wird die Ausführung von func
so schnell wie möglich geplant. Der Scheduler ruft es jedoch erst auf, nachdem das aktuell ausgeführte Skript abgeschlossen ist.
Daher ist die Funktion so geplant, dass sie „direkt nach“ dem aktuellen Skript ausgeführt wird.
Dies gibt beispielsweise „Hallo“ und dann sofort „Welt“ aus:
setTimeout(() => alarm("World")); alarm("Hallo");
Die erste Zeile „fügt den Anruf nach 0 ms in den Kalender ein“. Aber der Planer „überprüft den Kalender“ erst, nachdem das aktuelle Skript abgeschlossen ist, also steht "Hello"
zuerst und "World"
danach.
Es gibt auch fortgeschrittene browserbezogene Anwendungsfälle für Timeouts ohne Verzögerung, die wir im Kapitel Ereignisschleife: Mikrotasks und Makrotasks besprechen.
Nullverzögerung ist tatsächlich nicht Null (in einem Browser)
Im Browser gibt es eine Begrenzung, wie oft verschachtelte Timer ausgeführt werden können. Der HTML Living Standard sagt: „Nach fünf verschachtelten Timern muss das Intervall mindestens 4 Millisekunden betragen.“
Lassen Sie uns anhand des folgenden Beispiels demonstrieren, was es bedeutet. Der darin enthaltene setTimeout
Aufruf plant sich selbst ohne Verzögerung neu. Jeder Aufruf merkt sich die Echtzeit des vorherigen Aufrufs im times
Array. Wie sehen die tatsächlichen Verzögerungen aus? Mal sehen:
let start = Date.now(); let times = []; setTimeout(function run() { times.push(Date.now() - start); // Verzögerung vom vorherigen Aufruf merken if (start + 100 < Date.now()) alarm(times); // Verzögerungen nach 100 ms anzeigen sonst setTimeout(run); // sonst neu planen }); // ein Beispiel für die Ausgabe: // 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
Erste Timer laufen sofort (genau wie in der Spezifikation beschrieben), und dann sehen wir 9, 15, 20, 24...
. Die obligatorische Verzögerung von 4+ ms zwischen den Aufrufen kommt ins Spiel.
Ähnliches passiert, wenn wir setInterval
anstelle von setTimeout
verwenden: setInterval(f)
wird f
einige Male mit einer Verzögerung von Null und danach mit einer Verzögerung von 4+ ms ausgeführt.
Diese Einschränkung stammt aus der Antike und viele Skripte basieren auf ihr, daher existiert sie aus historischen Gründen.
Für serverseitiges JavaScript besteht diese Einschränkung nicht und es gibt andere Möglichkeiten, einen sofortigen asynchronen Job zu planen, wie setImmediate für Node.js. Dieser Hinweis ist also browserspezifisch.
Die Methoden setTimeout(func, delay, ...args)
und setInterval(func, delay, ...args)
ermöglichen es uns, die func
einmal/regelmäßig nach delay
auszuführen.
Um die Ausführung abzubrechen, sollten wir clearTimeout/clearInterval
mit dem von setTimeout/setInterval
zurückgegebenen Wert aufrufen.
Verschachtelte setTimeout
Aufrufe sind eine flexiblere Alternative zu setInterval
und ermöglichen es uns, die Zeit zwischen den Ausführungen genauer festzulegen.
Die Planung ohne Verzögerung mit setTimeout(func, 0)
(dasselbe wie setTimeout(func)
) wird verwendet, um den Aufruf „so schnell wie möglich, aber nachdem das aktuelle Skript abgeschlossen ist“ zu planen.
Der Browser begrenzt die minimale Verzögerung für fünf oder mehr verschachtelte Aufrufe von setTimeout
oder für setInterval
(nach dem 5. Aufruf) auf 4 ms. Das hat historische Gründe.
Bitte beachten Sie, dass nicht alle Planungsmethoden die genaue Verzögerung garantieren können .
Beispielsweise kann der Browser-Timer aus vielen Gründen langsamer werden:
Die CPU ist überlastet.
Der Browser-Tab befindet sich im Hintergrundmodus.
Der Laptop befindet sich im Batteriesparmodus.
All dies kann die minimale Timer-Auflösung (die minimale Verzögerung) je nach Browser und Leistungseinstellungen auf Betriebssystemebene auf 300 ms oder sogar 1000 ms erhöhen.
Wichtigkeit: 5
Schreiben Sie eine Funktion printNumbers(from, to)
, die jede Sekunde eine Zahl ausgibt, beginnend bei from
und endend mit to
.
Machen Sie zwei Varianten der Lösung.
Verwenden von setInterval
.
Verwendung von verschachteltem setTimeout
.
Mit setInterval
:
Funktion printNumbers(from, to) { let current = from; let timerId = setInterval(function() { Warnung (aktuell); if (current == to) { clearInterval(timerId); } aktuell++; }, 1000); } // Verwendung: printNumbers(5, 10);
Verwendung von verschachteltem setTimeout
:
Funktion printNumbers(from, to) { let current = from; setTimeout(function go() { Warnung (aktuell); if (aktuell < bis) { setTimeout(go, 1000); } aktuell++; }, 1000); } // Verwendung: printNumbers(5, 10);
Beachten Sie, dass es bei beiden Lösungen zu einer anfänglichen Verzögerung vor der ersten Ausgabe kommt. Die Funktion wird beim ersten Mal nach 1000ms
aufgerufen.
Wenn wir möchten, dass die Funktion auch sofort ausgeführt wird, können wir einen zusätzlichen Aufruf in einer separaten Zeile hinzufügen, etwa so:
Funktion printNumbers(from, to) { let current = from; Funktion go() { Warnung (aktuell); if (current == to) { clearInterval(timerId); } aktuell++; } gehen(); let timerId = setInterval(go, 1000); } printNumbers(5, 10);
Wichtigkeit: 5
Im folgenden Code ist ein setTimeout
-Aufruf geplant, dann wird eine umfangreiche Berechnung ausgeführt, deren Abschluss mehr als 100 ms dauert.
Wann wird die geplante Funktion ausgeführt?
Nach der Schleife.
Vor der Schleife.
Am Anfang der Schleife.
Was wird alert
angezeigt?
sei i = 0; setTimeout(() => alarm(i), 100); // ? // Gehen Sie davon aus, dass die Zeit zum Ausführen dieser Funktion >100 ms beträgt for(let j = 0; j < 100000000; j++) { i++; }
Jeder setTimeout
wird erst ausgeführt, nachdem der aktuelle Code abgeschlossen ist.
Das i
wird das letzte sein: 100000000
.
sei i = 0; setTimeout(() => alarm(i), 100); // 100000000 // Gehen Sie davon aus, dass die Zeit zum Ausführen dieser Funktion >100 ms beträgt for(let j = 0; j < 100000000; j++) { i++; }