mitmario.dev

setTimeout und setInterval

JavaScript Im Browser 5 Min Lesezeit 3 BeispieleLektion 1 von 8

Bis hierhin lief dein Code von oben nach unten durch, und wenn er fertig war, war er fertig. Ab dieser Lektion kann Code auch später laufen.

Das braucht man ständig: eine Meldung, die nach drei Sekunden wieder verschwindet, eine Uhr, die weiterläuft, eine Suche, die erst startet, wenn der Benutzer aufgehört hat zu tippen.

Die beiden Werkzeuge dafür heißen setTimeout und setInterval.

setTimeout

setTimeout bekommt zwei Dinge: eine Funktion und eine Anzahl Millisekunden. Nach dieser Zeit wird die Funktion aufgerufen, genau einmal.

Zurück bekommst du eine Kennung. Mit clearTimeout(kennung) kannst du den Termin absagen, solange er noch nicht dran war.

Tausend Millisekunden sind eine Sekunde. Das klingt banal, ist aber die häufigste Verwechslung am Anfang: setTimeout(fn, 3) wartet drei Tausendstel und nicht drei Sekunden.

Die Zeile danach läuft zuerst

Die Zeile danach läuft zuerst
console.log("erste Zeile");

setTimeout(() => {
  console.log("nach 300 ms");
}, 300);

setTimeout(() => {
  console.log("nach 0 ms");
}, 0);

console.log("letzte Zeile");

Hier steckt der Punkt, an dem sich entscheidet, ob du den Rest des Abschnitts verstehst.

setTimeout wartet nicht. Der Aufruf meldet den Termin an und ist sofort fertig. Die Zeile darunter läuft also im selben Atemzug weiter, und die angemeldete Funktion kommt irgendwann später.

Im ersten Beispiel siehst du die Folge davon: In der Console steht zuerst „erste Zeile”, dann „letzte Zeile”, dann „nach 0 ms”, und ganz zum Schluss „nach 300 ms”.

Besonders das dritte ist der Aha-Moment. Selbst bei null Millisekunden läuft die Funktion erst, wenn der übrige Code durch ist. setTimeout(fn, 0) heißt nicht „jetzt”, sondern „so bald wie möglich, aber nicht jetzt”. Warum das so ist, erklärt die nächste Lektion.

Im Reiter „Debug” steht dasselbe an deinen eigenen Zeilen. Drück dort auf Aufzeichnen, und du bekommst sechs Schritte. Erst laufen die Zeilen 1, 3, 7 und 11 durch, also alles, was direkt in der Datei steht. Die beiden setTimeout sind darin zwei Zeilen wie jede andere: Termin anmelden, fertig, nächste Zeile. Danach springt die Liste zurück nach oben auf Zeile 8, und ganz zum Schluss auf Zeile 4.

Die Console sagt dir, was ausgegeben wurde. Der Reiter sagt dir, welche Zeile wann dran war, und darum geht es hier.

setInterval

Ein Intervall, das sich selbst beendet
const anzeige = document.getElementById("anzeige");
let durchlauf = 0;

// setInterval liefert eine Kennung zurück. Ohne sie käme man
// nie wieder heraus.
const kennung = setInterval(() => {
  durchlauf = durchlauf + 1;
  anzeige.textContent = `Durchlauf ${durchlauf}`;
  console.log("Durchlauf", durchlauf);

  if (durchlauf === 5) {
    clearInterval(kennung);
    anzeige.textContent = "fertig nach 5 Durchläufen";
    console.log("Intervall gestoppt");
  }
}, 200);

setInterval funktioniert genauso, wiederholt sich aber. Alle so und so viele Millisekunden läuft die Funktion erneut, und zwar so lange, bis jemand sie stoppt.

Gestoppt wird mit clearInterval(kennung), und die Kennung ist genau der Wert, den setInterval zurückgegeben hat. Ohne diese Kennung kommst du nicht mehr heraus. Deshalb schreibt man sie sich immer in eine Variable, auch wenn man im Moment nicht vorhat, das Intervall je zu stoppen.

Im Beispiel zählt das Intervall bis fünf und beendet sich dann selbst, aus seiner eigenen Funktion heraus.

Zeichne es einmal auf. Im Protokoll stehen 26 Schritte, und der Rumpf des Intervalls kommt darin fünfmal vor, jedes Mal mit durchlauf um eins höher. Der drittletzte Schritt ist das clearInterval, und danach hört die Liste auf. Ein Intervall, das sich nicht selbst stoppt, hört an dieser Stelle eben nicht auf, und genau darum geht es im nächsten Abschnitt.

Aufräumen ist nicht optional

Ein vergessenes Intervall läuft weiter. Es läuft weiter, wenn der Benutzer die Ansicht verlassen hat, es läuft weiter, wenn das Element, das es aktualisiert, längst aus dem Dokument entfernt wurde, und es läuft weiter, bis die Seite neu geladen wird.

Die typischen Folgen davon:

  • Der Rechner arbeitet für nichts, der Lüfter geht an, der Akku ist schneller leer.
  • Zwei Intervalle laufen gleichzeitig, weil jemand zweimal auf Start gedrückt hat, und die Anzeige zählt doppelt so schnell.
  • Ein Handler fasst ein Element an, das nicht mehr da ist, und wirft TypeError: Cannot read properties of null.

Die Regel dazu ist kurz: Wer ein Intervall startet, ist dafür zuständig, es wieder zu stoppen. Bei einem Startknopf heißt das, vor dem Start zu prüfen, ob nicht schon eines läuft.

Die Zeit ist ein Mindestwert

Der Mindestwert ist wirklich ein Mindestwert
const start = Date.now();

setTimeout(() => {
  console.log("Angefordert waren 50 ms, gedauert hat es:", Date.now() - start, "ms");
}, 50);

// Der Stapel ist jetzt für eine Weile besetzt. Der Timer darf
// erst dran, wenn diese Schleife fertig ist.
let summe = 0;
for (let i = 0; i < 40000000; i++) {
  summe = summe + i;
}

console.log("Schleife fertig nach", Date.now() - start, "ms");

setTimeout(fn, 50) verspricht nicht, dass die Funktion nach 50 Millisekunden läuft. Es verspricht, dass sie nicht früher läuft.

JavaScript arbeitet eine Sache zur Zeit ab. Ist der Rechner gerade mit etwas anderem beschäftigt, wartet der Timer, bis er drankommt. Im dritten Beispiel siehst du das in aller Deutlichkeit: Eine Schleife besetzt den Rechner für ein paar hundert Millisekunden, und der Timer, der nach 50 dran wäre, kommt erst danach.

Für die Praxis heißt das zweierlei. Erstens: Verlass dich nie darauf, dass eine feste Zahl Durchläufe einer festen Zeit entspricht. Eine Stoppuhr, die bei jedem Intervall zehn Millisekunden dazuzählt, geht nach einer Minute merklich falsch. Lektion 15.7 zeigt, wie man es stattdessen macht.

Zweitens: Wenn ein Browser hakt, ist fast immer eine Schleife schuld, die zu lange rechnet. Der Timer ist dann nur das Opfer.

Hier hilft der Reiter „Debug” ausnahmsweise nicht, und das ist einmal wert, gesehen zu werden. Die Schleife im dritten Beispiel macht 40 Millionen Durchläufe, und aufgezeichnet wird jeder einzelne davon. Nach 2000 Schritten ist das Protokoll voll, es sagt das auch („Ab hier wurde nicht weiter aufgezeichnet”), und was dann fehlt, ist ausgerechnet der Timer am Ende. Die Antwort auf die Frage dieser Lektion steht in der Console, und zwar als Zahl.

Die Funktion mitgeben, nicht aufrufen

Ein Fehler zum Schluss, der jedem einmal passiert:

setTimeout(zeigeMeldung, 1000) gibt die Funktion mit. Sie läuft nach einer Sekunde.

setTimeout(zeigeMeldung(), 1000) ruft sie sofort auf und gibt ihr Ergebnis mit. Nach einer Sekunde versucht der Browser dann, dieses Ergebnis aufzurufen, und meistens ist das undefined.

Der Unterschied sind zwei Klammern. Wenn ein Timer sofort feuert statt später, ist das die erste Stelle, an der du nachsiehst.

Zum Mitnehmen

Die Millisekundenangabe ist ein Mindestwert, keine Zusage. setTimeout(fn, 0) heißt so bald wie möglich, aber nicht jetzt.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe im Editor, direkt im Browser geprüft

    Öffnet sich mit dem Basis Konto.