JavaScript/Tutorials/Timer und Countdown

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

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.

Ein verzögerter Aufruf sieht in manch anderen Sprachen schematisch so aus:

Beispiel
befehl1;
sleep(5);
befehl2;

In JavaScript hingegen notieren wir eine Funktion, die wir zeitverzögert aufrufen:

Beispiel
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:

Beispiel
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:

Beispiel
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

Beispiel
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:

Beispiel
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:

Beispiel
  // 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).

Beispiel
  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.

Beispiel
  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.

Beispiel
  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.


Beispiel ansehen …
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:

Beispiel
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:

Beispiel
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:

Beispiel
      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.

Beispiel
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
      • (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

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.

Siehe auch