tags:

views:

168

answers:

3

I use the following code to create countdowns in Javascript. n is the number of times to repeat, freq is the number of milliseconds to wait before executing, funN is a function to call on each iteration (typically a function that updates part of the DOM) and funDone is the function to call when the countdown is complete.

function timer(n, freq, funN, funDone)
{
 if(n == 0){
  funDone();
 }else{
  setTimeout(function(){funN(n-1); timer(n-1, freq, funN, funDone);}, freq);  
 }
}

It can be called like so:

    timer(10,
      1000, /* 1 second */
      function(n){console.log("(A) Counting: "+n);},
      function() {console.log("(A) Done!");}
     );

    timer(10,
      500,
      function(n){console.log("(B) Counting: "+n);},
      function() {console.log("(B) Done!");}
     );

The advantage of this is that I can call timer() as many times as I want without worrying about global variables etc. Is there a better way to do this? Is there a clean way to make setInterval stop after a certain number of calls (without using global variables)? This code also creates a new lambda function with each call to setTimeout which seems like it could be problematic for large countdowns (I'm not sure how javascript's garbage collector handles this).

Is there a better way to do this? Thanks.

+2  A: 

I'd create an object that receives a counter and receives a function pointer to execute, something akin to the following pseudo code:

TimedIteration = function(interval, iterations, methodToRun, completedMethod){

  var counter = iterations;
  var timerElapsed = methodToRun;  //Link to timedMethod() method
  var completed = callbackMethod;

  onTimerElapsed = function(){
    if (timerElapsed != null)
      timerElapsed();
  }

  onComplete = function(){
    if (completed != null)
       completed();
  }

  timedMethod = function(){
    if (counter != null)
      if (counter > 0) {
        setTimeOut(interval, onTimerElapsed);
        counter--;
      }
      else
        onComplete();
      this = null;
    }
  }

  if ((counter != null)&&(counter > 0)){
    //Trip the initial iteration...
    setTimeOut(interval, timedMethod);
    counter--;
  }  
}

obviously this is pseudo code, I've not tested it in an IDE and syntactically I'm not sure if it'll work as is [I'd be astonished if it does], but basically what you're doing is you're creating a wrapper object that receives a time interval, a number of iterations and a method to run upon the timer elapsed.

You'd then call this on your method to run like so:

function myMethod(){
  doSomething();
}

function doWhenComplete(){
  doSomethingElse();
}

new TimedIteration(1000, 10, myMethod, doWhenComplete);
BenAlabaster
+4  A: 

This is basically the same idea as @balabaster, but it is tested, uses prototype, and has a little more flexible interface.

var CountDownTimer = function(callback,n,interval) {
     this.initialize(callback,n,interval);
}

CountDownTimer.prototype = {
     _times : 0,
     _interval: 1000,
     _callback: null,
     constructor: CountDownTimer,
     initialize: function(callback,n,interval) {
                     this._callback = callback;
                     this.setTimes(n);
                     this.setInterval(interval);
                 },
     setTimes: function(n) {
                     if (n)
                         this._times = n
                     else
                         this._times = 0;
                 },
     setInterval: function(interval) {
                     if (interval)
                         this._interval = interval
                     else
                         this._interval = 1000;
                 },
     start: function() {
                     this._handleExpiration(this,this._times);
                 },
     _handleExpiration: function(timer,counter) {
                     if (counter > 0) {
                        if (timer._callback) timer._callback(counter);

                        setTimeout( function() {
                                           timer._handleExpiration(timer,counter-1);
                                          },
                                          timer._interval
                                      );
                     }
                 }
};

var timer = new CountDownTimer(function(i) { alert(i); },10);

...

<input type='button' value='Start Timer' onclick='timer.start();' />
tvanfosson
onComplete callback left as an exercise. :-)
tvanfosson
+2  A: 

I like your original solution better than the proposed alternatives, so I just changed it to not create a new function for every iteration (and the argument of fun() is now the value before decrement - change if needed...)

function timer(n, delay, fun, callback) {
    setTimeout(
        function() {
            fun(n);
            if(n-- > 0) setTimeout(arguments.callee, delay);
            else if(callback) callback();
        },
        delay);
}
Christoph
This is much more practical given the limitations of setTimeout (namely that it can only take a function, not an object, with no arguments). Consider adding a "scope" and "args" parameters to apply the function to an object, and give it arguments.
Jason S
Adding scope and args can be easily done, but I don't think it's necessary - I would 'bind' these to fun() via a seperate helper function...
Christoph
This is interesting. I would expect n to be bound to the initial value called with timer, but that doesn't happen.
Bill Zeller