Best practice: Poll instead of a setTimeout hack

Very often you'll have events happening asynchronously, but you need to wait until one has completed before you fire the second.
And you may not have the ability of attaching a callback function to the first.

In my less wise days I'd say "Lets just setTimeout it for a couple seconds..." but always felt really dirty about it.

A classier approach I've used lately is to poll for a change. Here I'm using the gmail greasemonkey API and waiting for it to load in before I start using it:

gmonkey.load("1.0");
 
// this is a self-executing anonymous function that uses setTimeout to call itself 
// at 50ms intervals until the isLoaded variable resolves as true.
 
(function(){
  if (gmonkey.isLoaded){
    // do stuff i want to do with the API
    gmonkey.get('1.0').addNavModule('notepad', '<iframe src="http://aaronboodman.com/halfnote/"></iframe>');
  } else {
    setTimeout(arguments.callee,50);
  }
})();

I found myself doing this a lot when loading in multiple external resources and playing with them.. So to generalize the code I wrote executeWhenLoaded():

// executeWhenLoaded() will be overloaded with as many arguments as i want to check for presence.
function executeWhenLoaded(func){  
 
  for (var i = 1; i<arguments.length; i++){ // for loop starts at 1 to skip the function argument.
    if (! window[ arguments[i] ]) {
      setTimeout(arguments.callee,50);      
      return;
    } 
  }
 
  func(); // only reaches here when for loop is satisfied.
}
 
// and in use:
 
executeWhenLoaded(function(){ 
    console.log(session.data);
},'session');   // session will return a value when the whatever preceding functionality is done.

executeWhenLoaded's first argument is the function to call, it can an unlimited number of arguments, which are all strings that reflect objects in the global namespace that have to be present in order to execute that function.

Update: In the comments, ProggerPete notes that this is not cross-browser compatible.. yet! In IE6, at least, the browser loses the original reference to the arguments object when it cycles through on the arguments.callee call. He offers a fix below.

7 comments ↓

#1 Lewis on 05.13.08 at 3:41 pm

mootools has a similar thing but it's only for AJAX calls (I think):

http://demos.mootools.net/Group

#2 ProggerPete on 05.14.08 at 10:47 am

The idea is good, but it doesn't seem to work (at least under IE6) as it loses the arguments when called by the timeout.

This seems to do the trick though...

function executeWhenLoaded(func){  
	var intRef, argRef = arguments;
 
	function executeCheck()	{
		// for loop starts at 1 to skip the function argument.
		for (var i = 1; i<argRef.length; i++){
			if (! window[ argRef[i] ]){
				if (!intRef) intRef = setInterval(executeCheck,50);      
				return;
			}
		} 
		clearInterval(intRef);
		func(); // only reaches here when for loop is satisfied.
	};
	executeCheck();
}
 
function setFoo(){ window.foo = "Tricksy"; }
function showFoo(){ alert(window.foo); }
 
setTimeout(setFoo, 1000);
 
executeWhenLoaded(showFoo, "foo");
#3 Paul Irish on 05.14.08 at 11:16 am

Excellent suggestion, Pete.

I must admit I was using this technique in a greasemonkey script and therefore in FF only.

Will update the post.

#4 Adam J. McIntyre on 05.14.08 at 1:17 pm

I sped things up a bit below. Basically, if we know an argument exists, we can shift( ) it right off the arguments array, saving a bunch of iterations on that for (or in this case, while) loop.

            function executeWhenLoaded(f,args){
                if(!args){
                    arguments.shift();
                    var args = arguments;  
                } 
                while(args.length &gt; 0){
                    if(window[args[0]]){ args.shift(); }
                    else{
                        setTimeout(function(){
                            executeWhenLoaded(f,args);
                        },50)
                    }
                }
                f();
            }

Caveat: untested. :)

#5 Harald on 05.15.08 at 1:05 pm

If you need to load JavaScript files (or CSS, whatever) MooTools provides the Asset helper. It adds a cross-browser onload event, no polling or callback from the JS file needed.

Polling example from MooTools DomReady:

var temp = document.createElement('div');
(function(){
	($try(function(){
		temp.doScroll('left');
		return $(temp).inject(document.body).set('html', 'temp').dispose();
	})) ? domready() : arguments.callee.delay(50);
})();
#6 Paul Irish on 05.22.08 at 10:45 am

btw- You can consider my code MIT licensed.
Nah, fuck it. It's public domain. Go wild.

#7 Tom McDonald on 06.24.08 at 11:31 am

Adam McIntyre -- I tested your code in FireFox 3.0 and got 'shift' is not a command. I suppose you're implementing an extension, if so please describe it. Speed test results would be nice too :-)

Leave a Comment

Basic HTML is cool. Surround code blocks with <pre lang="javascript"></pre>.