EDITORIAL
REPORT SVG Open 2011
SPOTS ON Mappetizer
JAVASCRIPT Timer Class
ARTIST'S CORNER Interview: Art Science Factory

JAVASCRIPT

Timer Class

Author: DS
Published: November 2011

Time Flies, You Can't, They Go Too Fast

But we can try to write a Timer class. And this was the only funny bit in this article where you could relax.

The need for a timer class is to gain full control over the timers created with the setTimeout() and setInterval() methods of the window object. The objective is to provide an API designed to fulfill the needs that any experienced developer has encountered while dealing with timers. When we look up timers in JavaScript code repositories or libraries we can see that practically all serious proposals use some method for storing and managing references to the timers. We will see that this is unnecessary, and we will also see that to destroy instances systematically is not the best practice.

The class we are going to write does not use the window.requestAnimationFrame method.1

Before defining the requirements that we want our Timer class to meet, we can make an important consideration about timers usage: more often than not timer instances are meant to be reusable! A typical example is a scroll button, while the application is running the user is likely to use that button several times; animations are often meant to be replayed, perhaps with some variation. My experience tells me that re-usability is sought for at least 60% and perhaps up to 90% of the cases. Enough to establish a principle of timer re-usability.

Now, wouldn't it be smart to be able to reuse a timer without needing to re-instantiate, and, with the possibility to modify its behavior?

Our Timer class must meet these requirements:

To Prototype Or Not To Prototype

Now we are left with that terrible dilemma. If we define prototype methods we save memory but we are forced to store references to the instances. If instead we define instance methods, then each instance will have its own copy of the methods, with consequent growth of used memory. On the other hand, this technique allows us to define local variables (properties of the Activation object, also known as private member variables to C++ coders) in the constructor and to store a reference to the instance and other information into those variables, thanks to which the window timer methods can then trace the objects referenced by these local variables. The second solution wins the match thanks to its greater agility, and to nurse our memory we will define a destructor as a class method. One last consideration before proceeding: a Timer class should not manage a queue to constantly check if a timer instance is already running and put in queue the odd crude request. That approach satisfies one particular need but precludes a few other possibilities that the developer may seek. In the object oriented environment that the class establishes, an instance is enabled with self-management and the developer has full control over the state of the instances that were created. The properties of a timer instance and the variable or object property that created the instance, as well as the properties of the object itself (or of a substitutive object designated ad hoc to handle the timer), can always be queried.

The Constructor

var Timer = function () {		
  var self,
      handle;

  return {
    initialize : function () {
      var o = arguments[0];
      for (var prop in o) this[prop] = o[prop];
      var f = "frequence" in this,
          d = "delay" in this;

      self = this;
      handle = this.handle || this;
      if (handle != this) handle.callback = this.callback;

      if (f && d) this.timeout = window.setTimeout(this.setInterval, this.delay);
      else if (f) this.setInterval();
      else this.timeout = window.setTimeout(function () {handle.callback(self);}, this.delay);
      return this;
    },

    setInterval : function () {
      self.interval = window.setInterval(function () {handle.callback(self);}, self.frequence);
    },

    clear : function () {
      if (this.timeout) window.clearTimeout(this.timeout);
      if (this.interval) window.clearInterval(this.interval);
      this.timeout = this.interval = null;
      if (this.destroy) Timer.destroy(handle, this.destroy);
    }
  }
};

Timer.destroy = function (handle, name) {
  if (handle[name]) delete handle[name];
  else if (name in window) {
    if (! delete window[name]) window[name] = null;
  }
};

Throughout its life-cycle, that is, until and if the instance is destroyed, the variable self is a reference to the Timer instance and the variable handle is a reference to an object.

Any properties that we define during instantiation are inherited by the instance, which is passed as parameter to the callback function (variable self).

If a handle object is not specified, the variable handle is a reference to the Timer instance and in this case the this keyword and the variable name designating the parameter self in the callback function, are synonyms.

The function defines three instance methods: initialize ; setInterval ; clear .

The class method destroy deletes a property and attempts to delete a variable that does not have the DontDelete attribute (note that this should throw an error in old versions of IE if you create a global variable by explicit assignment: this.timer = ...; where this refers to the window object or the host object).

The type of test for frequence and delay allows to assign the value 0 (zero).

Instantiation

To create a Timer instance we pass an object to the initialize method, where the property callback is required, and at least one of the properties delay or frequence is required. Note that the structure of this Timer class allows instantiation with or without the new operator; it will not make any difference.

var myTimer = Timer()
.initialize({
  callback : Function,
  delay : 500,
  frequence : 50
});

delay/frequence cases:

  1. delay only is set: window.setTimeout is used.
  2. delay and frequence are set: window.setTimeout and window.setInterval are used.
  3. frequence only is set: window.setInterval is used.

If the property handle is set, the callback function is invoked as a method of handle. We can assign any object to this property, not necessarily that through which we are instantiating the Timer. We have seen that if this property is not set, the variable handle references the timer instance.

The property destroy, if set, gets a string designating the name of the variable or property to which we are assigning the Timer instance. In this case only, the instance will be destroyed upon termination of the timed process.

var myTimer = Timer()
.initialize({
  ...
  handle : Object,
  destroy : "myTimer"
});

As we have already seen, these and any other properties that we define in the object that we pass to the initialize method, become properties of the Timer instance, which is itself passed as parameter to the callback function, where we can process those properties.

obj.timer2 = Timer()
.initialize({
  callback : tick,
  delay : 1000,
  frequence : 40,
  handle : obj,
  target : {
    text: document.getElementById("T2"),
    rect: document.getElementById("R2")
  },
  counter: 0,
  repeatCount: 0,
  repeats: 3
});

We can now resume that:

  1. All the properties set in the parameter object are inherited by the instance.
  2. The properties callback, delay, frequence, handle and destroy are processed by the Timer methods.
  3. The properties timeout and, or interval of the Timer instance are assigned dynamically.
  4. If a handle object is specified, it inherits the property callback. The function referenced by callback is then an instance method of the handle object. If handle is not specified, the callback function is executed as an instance method of the timer object.
  5. Any other properties are user define.

Regarding point 5, if for example we needed the current time we would just assign Date.now() – or a +new Date() shim – to a property.

Before taking a look at the examples, where we will see how all the requirements are satisfied, we must take note that there is a restriction to the second requirement “Multiple concurrent timers”. If we plan to specify the same handle object for two or more concurrent Timer instances, but different callback functions, because the function is assigned to the callback property (dynamically defined) of the handle object, the latest callback definition will override any previously defined one. This means that there is only one callback function for a given object at any given time. The function will then be a handler through which we can dispatch to other functions or methods.

This actually opens a number of possibilities, like for example defining a new object for the purpose, with one unique property referencing the object that is instantiating the timers, then declare for one of the timers this new object as handle and a different callback function. The interaction could get quite interesting: two (or more!) controllers for one animation...

Example

In this SVG [1] or HTML+SVG [2] example two timers are instantiated by one object and two more by global variables. Let's see them in detail.

We define an object with two properties that we will use in the callback function:

var obj = {
  message : 'done',
  relaunch : function() {
    delete this.delay;
    if (this.repeatCount == (this.repeats -1)) {
      this.destroy = "timer2";
      this.delay = 1000;
    }
    this.initialize({
      frequence : 20 / this.repeatCount,
      counter: 0
    });
  }
};

The first Timer instance of obj:

obj.timer1 = Timer()
.initialize({
  callback : tick,
  frequence : 50,
  handle : obj,
  destroy: "timer1",
  target : {
    text: document.getElementById("T1"),
    rect: document.getElementById("R1")
  },
  counter: 0,
  end: 200
});

This timer will start with no delay and will be destroyed because we don't plan on reusing it.

The second Timer instance of obj:

obj.timer2 = Timer()
.initialize({
  callback : tick,
  delay : 1000,
  frequence : 40,
  handle : obj,
  target : {
    text: document.getElementById("T2"),
    rect: document.getElementById("R2")
  },
  counter: 0,
  repeatCount: 0,
  repeats: 3,
  end: 200
});

We plan on reusing this timer. The process repeats three times. The second time with no delay and a faster frequence, and the third time with a delay of 1 second and yet a faster frequence, as set in the relaunch method defined in obj. That method is invoked in the callback function two times after the first run. The instance is destroyed after the last run. In this respect, note that if we want to delete a timer after a number of uses, the property destroy must be set before the last use.

The third Timer instance is assigned to a global variable:

var timer1 = Timer()
.initialize({
  callback : tick,
  delay : 2000,
  frequence : 30,
  target : {
    text: document.getElementById("T3"),
    rect: document.getElementById("R3")
  },
  counter: 0,
  end: 200
});

For this timer we have specified the same callback function that we defined for the two timers above. This is definitely an aggravation in terms of performance and readability, but it shows the use of a handler through which we can dispatch to other functions. While this is an obligation for timer instances that share the same handle object, in this case it's a choice. Also, we don't plan to reuse this timer but we're not quite sure. In the doubt we can afford to spare its life, we don't set the property destroy. But if we were running an application with a large number of timers, the choice could be critical.

The fourth Timer instance, also assigned to a global variable:

var timer2 = Timer()
.initialize({
  callback : tick,
  delay : 3000,
  frequence : 20,
  target : {
    text: document.getElementById("T4"),
    rect: document.getElementById("R4")
  },
  counter: 0,
  end: 100
});

For this timer we have again specified the same callback function. At the end of its execution, specified by the property end, the properties callback, frequence and end are overridden, and the process is relaunched. For the second run callback points to the step_tick function. We also set a new property named step which will change the behavior of the process.

Here are defined the tick and step_tick callback functions.

function tick(timer) {
  var t = timer.target;
  t.text.firstChild.data = ++timer.counter;
  t.rect.setAttributeNS(null, "width", timer.counter);
  if (timer.counter == timer.end) {
    timer.clear();
    if (timer == timer2) {
      delete this.delay;
      this.initialize({
        callback : step_tick,
        frequence : 1000,
        end: 200,
        step: 10
      });
    }
    if (this.message) t.text.firstChild.data = this.message;
    if (timer == this.timer2) {
      if (++timer.repeatCount == timer.repeats) {
        timer.clear();
        return;
      }
      timer.clear();
      this.relaunch.call(timer);
    }
  }
}

function step_tick(timer) {
  var t = timer.target;
  t.text.firstChild.data = (timer.counter += timer.step) - timer.end;
  t.rect.setAttributeNS(null, "width", timer.counter);
  if (timer.counter == timer.end) timer.clear();
}

Recycling

We have seen that the life cycle of a Timer instance does not terminate unless we set the property destroy, or re-affect or manually delete the variable or property to which it was assigned. This means that its properties keep their values, with the exception of timeout and, or interval which are set to null in the clear method, and reassigned dynamically on each subsequent run. If then we want to restart a timer instance, to loop an animation for example, we can relaunch the process by invoking the initialize method with the current property values, or we can override the properties, including callback. We can also define new properties that will affect the timed process on subsequent runs.

Compatibility

This Timer class is not compatible with the Adobe SVG plug-in (ASV) in IE5-6-7-8.

Notes

1 Its API is still under development and at this stage there seem to be differences in implementation. There are also a few catches. It's definitely animation oriented, and anyone who's been following its evolution will have seen recent posts of users complaining about animations going faster than sought in this or that implementation, and asking ways to control the speed. Actually, back in February 2011 feedback was being asked on “It's currently modeled like setTimeout; does a setInterval repeating style API make more sense?”, and as late as a couple of weeks ago you could read this comment “Oh, Great! Somebody re-invented setInterval…”. It looks like the dog is biting its tail. In any case, besides animations there are other timed processes which need precise control, and we can't let the browsers control them blindfold. If the browsers offer hardware acceleration and repaint cycles it's good for us, but just like for SMIL timing, we must be able to keep total control on every aspect of the timed processes.