JavaScript/Tutorials/Timer und Countdown
In diesem Artikel von Mathias Schäfer wird eine Komfortable Timer-Funktion - ein Helferscript zum wiederholten Ausführen von Funktionen vorgestellt. Er erschien zuerst 2007 im Selfhtml-aktuell-Bereich. Heute können Sie Timer auch mit der Date.now() realisieren.
Vorüberlegungen
JavaScript-Programme sind nicht mit anderen Computerprogrammen zu vergleichen, die eine bestimmten Startzeit und Endzeit ihrer Ausführung haben. JavaScripte werden immer wieder für kurze Zeit aktiv, etwa wenn auf Ereignisse reagiert wird. Wenn ein JavaScript tatsächlich mehrere Sekunden an einem Stück läuft und die gesamten Rechenkapazitäten in Beschlag nimmt, friert der Browser ein oder zeigt einen Dialog, mit dem das Script abgebrochen werden kann.
Dementsprechend gibt es in JavaScript keinen Befehl sleep
, der einfach für eine gegebene Zeit nichts tut. So etwas ist in JavaScript nicht möglich. Allerdings lassen sich über window.setTimeout und window.setInterval Funktionen nach einer angegebenen Wartezeit aufrufen.
Soll ein Script über längere Zeit laufen und zwischen einzelnen Befehlen eine Wartezeit verstreichen lassen, ist dies in JavaScript nur mit window.setTimeout über das »Stop & Go«-Verfahren möglich. Auf diese Weise können z.B. Animationen umgesetzt werden.
Inhaltsverzeichnis
Ein verzögerter Aufruf sieht in manch anderen Sprachen schematisch so aus:
befehl1;
sleep(5);
befehl2;
In JavaScript hingegen notieren wir eine Funktion, die wir zeitverzögert aufrufen:
befehl1;
function befehl2 () {}
window.setTimeout(befehl2, 5000);
Knifflig wird es nun, wenn ein Befehl mit sich ändernden Parametern mehrmals ausgeführt werden soll und dazwischen jeweils eine Zeitspanne liegen soll. Ein Countdown etwa ließe sich in anderen Sprachen schematisch so realisieren:
for ($i = 10; $i > 0; i--) {
echo $i . "\n";
sleep(1);
}
In JavaScript wird dies üblicherweise mit einer Funktion gelöst, die sich immer wieder selbst verzögert aufruft und dabei den Schleifenzähler übergibt:
function countdown (i) {
if (i == undefined) {
// Startwert
i = 10;
}
alert(i);
if (i > 0) {
i--;
// Funktion verzögert aufrufen
var timeout = window.setTimeout('countdown(' + i + ')', 1000);
}
}
// Countdown anstoßen
countdown();
Wir sehen hier bereits, wie kompliziert die Umsetzung eines einfachen Countdowns wird. Diese verbreitete Umsetzung lässt sich zunächst durch den Einsatz von Closures vereinfachen. Damit lässt sich die Übergabe von Parametern einfacher gestalten: Anstatt window.setTimeout("countdown(" + i + ")", 1000);
können wir ebenso eine anonyme Funktion notieren, die als Closure wirkt und in der die lokale Variable i
zur Verfügung steht: window.setTimeout(function () { countdown(i) }, 1000);
. Dies ist vor allem sinnvoll, wenn mehr und komplexere Variablen übergeben werden müssen, die nicht so einfach in einen String linearisiert werden können.
Wenn mehrere Funktionen periodisch aufgerufen werden sollen, so ist es umständlich, denselben Mechanismus immer wieder neu zu notieren. Im Folgenden wird eine Helferfunktion vorgestellt, mit der sich solche Aufgaben komfortabel umsetzen lassen.
Vorstellung der Timer-Methode
Function.prototype.Timer = function (interval, calls, onend) {
var count = 0,
payloadFunction = this,
startTime = new Date();
var callbackFunction = function () {
return payloadFunction(startTime, count);
};
var endFunction = function () {
if (onend) {
onend(startTime, count, calls);
}
};
var timerFunction = function () {
count++;
if (count < calls && callbackFunction() != false) {
window.setTimeout(timerFunction, interval);
} else {
endFunction();
}
};
timerFunction();
};
Zum Verständnis der Funktionsweise ist es notwendig, sich vor Augen zu führen, dass Funktionen in JavaScript nichts anderes als Objekte sind, die sich im Grunde genauso wie andere Variablen verhalten können. Außerdem werden verschachtelte Funktionen und Closures an mehreren Stellen verwendet, um Variablen zwischen verschiedenen Funktionen zu teilen, ohne dass sie global verfügbar sind.
Die Helferfunktion wird an das prototype-Objekt des Function-Objekts geheftet. Dies nennt sich prototypische Erweiterung und hat den Effekt, dass durch die Zuweisung Function.prototype.methode = function () {};
alle Funktionen fortan über die notierte Methode als gewöhnliches Unterobjekt verfügen. Denn Funktionen stammen vom Function-Konstruktor ab und erben dessen Methoden. Ein einfacheres Beispiel der prototypischen Erweiterung:
Function.prototype.zeigeParameterzahl = function () {
alert("Diese Funktion erwartet " + this.length + " Parameter.");
};
function irgendeineFunktion (parameter1, parameter2, parameter3) {}
irgendeineFunktion.zeigeParameterzahl();
Die gezeigte Timer-Methode erwartet drei Parameter:
Parameter | Bedeutung |
---|---|
interval | Zeit zwischen den Aufrufen in Millisekunden |
calls | Anzahl der Aufrufe |
onend | Funktion, die am Ende der Wiederholungen gestartet werden soll (optional) |
Am Anfang der Funktion werden einige Variablen initialisiert:
// Zähler, wie oft die Funktion schon ausgeführt wurde, beginne bei 0.
var count = 0;
// Lege eine Referenz auf die Funktion an, die mehrfach ausgeführt werden soll.
// Dies ist nötig, damit in den folgenden notieren verschachtelten Funktionen
// ein Zugriff darauf möglich ist.
var payloadFunction = this;
// Speichere den Startzeitpunkt der Aufrufkette.
var startTime = new Date();
Der Rest besteht aus drei Funktionen, die als lokale Variablen gespeichert werden. Alle wirken als Closures, das heißt, sie werden nach dem Ende der Ausführung der Timer-Funktion nicht gelöscht, sondern haben weiterhin Zugriff auf die eben initialisierten Variablen. Zudem haben die drei Funktionen aus demselben Grund Zugriff aufeinander, sie liegen im selben abgeschlossenen Scope (Geltungsbereich).
var callbackFunction = function () {
return payloadFunction(startTime, count);
};
Die callbackFunktion wird aufgerufen, während der Timer läuft. Sie startet die eigentliche Funktion, die mehrfach ausgeführt werden soll (payloadFunction
). Ihr übergibt sie den Startzeitpunkt sowie die fortlaufende Nummer des wiederholten Aufrufes. Wenn die callbackFunction
das Ergebnis false zurückgibt, wird der Timer beendet. Das heißt, dass in der zu wiederholenden Funktion einfach return false
notiert werden kann, um den Timer zu beenden - callbackFunction
gibt diesen Rückgabewert durch.
var endFunction = function () {
if (onend) {
onend(startTime, count, calls);
}
};
endFunction
wird am Ende aller Wiederholungen aufgerufen. Sie kapselt die als Parameter entgegen genommene onend
-Funktion und übergibt ihr den Startzeitpunkt, die tatsächliche Anzahl der Wiederholungen sowie die ursprünglich übergebene Anzahl der Wiederholungen.
var timerFunction = function () {
count++;
var callbackReturnValue = callbackFunction();
if (count < calls && callbackReturnValue != false) {
window.setTimeout(timerFunction, interval);
} else {
endFunction();
}
};
Die Timer-Funktion schließlich erhöht den Zähler, führt die callbackFunction
sowie sich selbst aus, wenn das Limit an Aufrufen noch nicht erreicht ist und die callbackFunction
nicht false
ergeben hat. Im Falle eines Abbruchs oder bei Erreichen des Limits wird die endFunction
ausgeführt.
Am Ende der Timer-Methode wird schließlich die timerFunction
erstmalig angestoßen, der weitere Ablauf wird an sie übergeben.
Anwendung und Beispiel
Ein komplexeres Beispiel wird verdeutlichen, wie die vorgestellte Helferfunktion angewendet wird, wie sie abläuft und wieso sie derartig strukturiert ist.
Die beispielhafte Aufgabe ist ein einfacher Countdown, der in einem Element ausgegeben werden soll. Gleichzeitig sollen zwei dieser Countdowns laufen.
function leadingzero (number) {
return (number < 10) ? '0' + number : number;
}
function countdown (seconds, target) {
var element = document.getElementById(target);
var calculateAndShow = function () {
if (seconds >= 0) {
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
var s = seconds % 60;
element.innerHTML=
leadingzero(h) + ':' +
leadingzero(m) + ':' +
leadingzero(s);
seconds--;
} else {
return false;
}
};
var completed = function () {
element.innerHTML = "<strong>Liftoff!<\/strong>";
};
calculateAndShow.Timer(1000, Infinity, completed);
}
window.onload = function () {
new countdown(20, 'counter1');
new countdown(10, 'counter2');
};
Herzstück ist die Funktion countdown
, die als Konstruktor eines eigenen Objektes dient (wobei diesem Objekt keine öffentlichen Eigenschaften zugewiesen werden). Beim Laden der Seite werden mit new
zwei Countdown-Objekte mit verschiedenen Parametern erzeugt:
window.onload = function () {
new countdown(20, 'counter1');
new countdown(10, 'counter2');
};
Die countdown-Konstruktorfunktion erwartet zwei Parameter: seconds
ist die Sekundenzahl, von der aus heruntergezählt werden soll, und target
ist ein String mit der ID desjenigen Elements, in dem der Countdown angezeigt werden soll. In diesem Fall werden zwei Countdowns gestartet mit den Element-IDs counter1
und counter2
, einer mit zehn und einer mit zwanzig Sekunden Laufzeit.
Zunächst wird eine Referenz namens element
auf das Element angelegt, in dem der Countdown ausgegeben werden soll:
var element = document.getElementById(target);
Im Konstruktor wird nun eine verschachtelte Funktion namens calculateAndShow
notiert. Dies ist die eigentliche Funktion, die mehrfach aufgerufen werden soll. Durch den Closure-Effekt sind in ihr die lokalen Variablen von
countdown
über die verschiedenen Aufrufe hinweg verfügbar - dies wird zum Zugriff auf
seconds
und element
benötigt.
calculateAndShow
fragt ab, ob seconds
noch nicht bei 0 angekommen ist. Wenn nicht, rechnet es den Sekundenwert in das Format HH:MM:SS (Stunden, Minuten, Sekunden) um und schreibt diesen Wert ins Element:
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
var s = seconds % 60;
element.innerHTML=
leadingzero(h) + ':' +
leadingzero(m) + ':' +
leadingzero(s);
Schließlich wird mit seconds--;
der Variablenwert verringert. Wenn dieser beim nächsten Aufruf von calculateAndShow
bei 0 angekommen wird, wird false
zurückgegeben. Dies führt, wie oben beschrieben wurde, dazu, dass die Kette an Aufrufen abbricht.
Am Ende der Funktion countdown
wird die Methode Timer des
calculateAndShow
-Funktionsobjektes gestartet. Die Funktion hat die Methode durch die prototypische Erweiterung erhalten.
calculateAndShow.Timer(1000, Infinity, completed);
Als Parameter übergeben wir 1000 Millisekunden als Intervall-Länge, die vordefinierte Konstante Infinity
(unendlich) als Anzahl der Wiederholungen sowie die verschachtelte Funktion completed
, die nach den wiederholten Aufrufen gestartet wird. Natürlich soll calculateAndShow
nicht unendlich oft ausgeführt werden, wir übergeben ihr aber selbst die Kontrolle darüber, wann die Kette an Wiederholungen abgebrochen werden soll - in dem Fall gibt sie einfach false
zurück.
Folgendes Ablaufprotokoll zeigt die Funktionsweise der Countdowns im Beispiel:
- Aufruf countdown
- Aufruf Timer
- Aufruf timerFunction
- Aufruf callbackFunction
- Aufruf calculateAndShow
- starte Timeout
- Aufruf callbackFunction
- (Diese timerFunction-Aufrufe wiederholen sich bis zum letzten Aufruf. Dieser startet keinen neuen Timeout, sondern die endFunction:)
- Aufruf timerFunction
- Aufruf callbackFunction
- Aufruf calculateAndShow
- Aufruf endFunction
- Aufruf completed
- Aufruf callbackFunction
- Aufruf timerFunction
- Aufruf Timer
Hier zeigt sich, wie timerFunction
den Ablauf regelt und die Kontrolle behält, wenn sie einmal angestoßen wurde. Sie führt mithilfe von callbackFunction
die eigentliche Wiederholungsfunktion
calculateAndShow
aus und die Wiederholungen werden mit endFunction
beendet.
Nachwort
Die vorgestellte Funktion soll beispielhaft zeigen, wie fortgeschrittene Programmiertechniken genutzt werden können, um einen einfach benutzbaren Timer zu realisieren. Dazu gehören prototypische Erweiterung, objektorientierte Programmierung, Funktionen als Objekte sowie Closures.
Das Konzept hat selbstverständlich Vor- und Nachteile und ist ausbaufähig. Andere Umsetzungen wie etwa der periodicalExecuter aus dem JavaScript-Framework Prototype besitzen eine stop-Methode, mit der die Kette der Aufrufe auch »von außen« unterbrochen werden kann, anstatt nur durch die mehrfach aufgerufene Funktion selbst.
Es gibt zudem verschiedene Methoden, um Variablen von einer Ausführung zur anderen zu konservieren, ohne dass sie notwendigerweise im globalen Scope liegen müssen. Das Anwendungsbeispiel nutzt hier ein eigenes Objekt mit privaten Eigenschaften, die durch den Closure-Effekt in der wiederholten Funktion verfügbar sind, die ihrerseits eine private Methode ist.