Async JavaScript callbacks

June 3rd, 2012. Tagged: IE, JavaScript

Ah, asynchronous JavaScripts. Love 'em, hate 'em, but you gotta use them!

I have quite a few posts here on this blog about this stuff. Starting with something I considered an interesting hack to emulate PHP's require(). This was posted in 2005. (2005! That's ancient. That's only a year after gmail was introduced, and you know gmail has been around, like, always). Then there was this and this and this and this. These days dynamic SCRIPT tags are something common, mainly because of their non-blocking behavior that helps performance. This is all good.

I wanted to highlight the part where you load a script file and then execute a function once the file is done loading. A common, but kinda wrong pattern I've posted back at the time, and tweaked a little since, is like so:

var first_js = document.getElementsByTagName('script')[0];
var js = document.createElement('script');
js.src = file;
 
// normal browsers
js.onload = function () {
  alert('loaded!!');
};
 
// IE
js.onreadystatechange = function () {
  if (js.readyState in {complete: 1, loaded: 1}) {
    alert('loaded!!');
  }
};
 
 
first_js.parentNode.insertBefore(js, first_js);

Back at the time (2006) this was fine. The problem is now that since version 9, IE supports onload handler in script elements. But it also supports onreadystatechange for backwards compatibility.

In other words in IE9+ your callbacks will be executed twice. Not good.

Single callback

There are various ways to deal with this situation.

1. You can delete the onload callback in readystatechange, beacuse readystatechange is called first.

js.onreadystatechange = function () {
  if (js.readyState in {complete: 1, loaded: 1}) {
    callback();
    js.onload = null;
  }
};

2. You can use a single assignment to both

js.onload = js.onreadystatechange = function () {
  // stuff...
  js.onload = js.onreadystatechange = null;
 
};

The problem with both of these is that readystatechange is involved even in browsers that are modern (IE9+) and support onload. Feels a bit ugh.

3. You can sniff onload support

if (typeof js.onload !== 'undefined') {
  // good stuff..
} else {
  // onreadystatechange jazz
}

This works because old IEs will not have any onload property (hence undefined) while supporting browsers will have this property initially set to null.

Hmm, making a distinction between two falsy values null and undefined seems a little fragile. The next developer will come and say: "meh, what's with the typeof verbosity, let's just say if (js.onload)"... And the whole thing will fail.

4. (And this is my preferred method) is to sniff support using addEventListener.

It just happens so that IE9, which supports onload, is also the first IE browser that supports addEventListener.

The whole thing looks like:

var first_js = document.getElementsByTagName('script')[0];
var js = document.createElement('script');
js.src = file;
 
if (js.addEventListener) { // normal browsers
  js.addEventListener('load', function(){
    alert('done!!');
  }, false);
} else {
  js.onreadystatechange = function() { // old IEs
    if (js.readyState in {loaded: 1, complete: 1}) {
      js.onreadystatechange = null;
      alert('done!!');
    }
  };
}
 
first_js.parentNode.insertBefore(js, first_js);

Drawback is that you decide on a feature (script onload support) based on a different feature (addEventListener support). I can live with this. We're talking here about an exception for known legacy browsers and shouldn't be an issue going forward in this brave new world where everyone lives in piece and love and brotherhood and sisterhood and motherhood and all browsers support onload and addEventListener.

So anyway, choose your poison :)

Here's a test page that listens to everything so you can play in different browsers:
http://www.phpied.com/files/jsasync/loaded.html

BTW, notice that IE is the only browser that fires window.onload before the onload of the (slow) script. This is another thing to keep in mind and look out for.

Tell your friends about this post: Facebook, Twitter, Google+

Sorry, comments disabled and hidden due to excessive spam. Working on restoring the existing comments...

Meanwhile, hit me up on twitter @stoyanstefanov