DailyJS

Asynchronous Resource Loading Part 2

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks lmaf network

Asynchronous Resource Loading Part 2

Posted by Alex R. Young on .
Featured

tutorials frameworks lmaf network

Asynchronous Resource Loading Part 2

Posted by Alex R. Young on .
*Let's Make a Framework* is an ongoing series about building a JavaScript framework from the ground up. These articles are tagged with [lmaf](http://dailyjs.com/tags.html#lmaf). The project we're creating is called [Turing](http://github.com/alexyoung/turing.js). Documentation is available at [turingjs.com](http://turingjs.com/).

Last week in Asynchronous Resource
Loading
I introduced some
popular script loading libraries, and discussed how recent web standards
aim to give us more control over how scripts are loaded. This week I
want to explain how to programatically load scripts asynchronously in a
way that supports legacy browsers.

Resource loading can be split into three main concerns:

  1. Controlling how things are loaded
  2. Providing an API
  3. Supporting legacy browsers

Each script loader that I looked at seemed to place a different emphasis
on each of these areas. RequireJS has a lot of
code for its module-based API; LABjs has a simpler
chained API which is a little more focused on the first problem.

Script Insertion

In Loading Scripts Without
Blocking

by Steve Souders, various non-blocking loading techniques are compared.
Steve summarises with the following

In many situations, the Script DOM Element is a good choice. It works in all browsers, doesn't have any cross-site scripting restrictions, is fairly simple to implement, and is well understood. The one catch is that it doesn’t preserve execution order across all browsers.

Inserting script tags is the approach most libraries use
for basic asynchronous script loading. Preserving order is problematic
and requires the use of several different solutions to accomplish it.

The most basic script insertion technique looks like this:

(function(global) {
  var appendTo = document.head || document.getElementsByTagName('head');

  function require(scriptSrc, fn) {
    var script = document.createElement('script');

    script.type = 'text/javascript';
    script.src = scriptSrc;
    script.onload = fn;

    appendTo.insertBefore(script, appendTo.firstChild);
  }

  turing.require = require;
}(window));

The handling of appendTo is based on
LAB.src.js. To create a suitable unit test, I've used the Turing Ajax test harness. The
test itself just needs to ensure JavaScript gets loaded and executed as
expected:

'test require': function() {
  $t.require('/load-me.js', function() {
    assert.equal(loadMeDone, 1);
  });
}

The load-me.js script sets a global,
loadMeDone, and the assertion tests for this. The script's
onload method will be set to this callback.

Internet Explorer Support

Internet Explorer provides an onreadystatechange property
instead of onload. That means the script element's
readyState has to be checked.

(function(global) {
  var appendTo = document.head || document.getElementsByTagName('head');

  function require(scriptSrc, fn) {
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = scriptSrc;

    script.onload = script.onreadystatechange = function() {
      if (!script.readyState || (script.readyState === 'complete' || script.readyState === 'loaded')) {
        script.onload = script.onreadystatechange = null;
        fn();
        appendTo.removeChild(script);
      }
    };

    appendTo.insertBefore(script, appendTo.firstChild);
  }

  turing.require = require;
}(window));

This creates an onload/onreadystatechange callback that should work in
both Internet Explorer and standards-compliant browsers. However,
running the test produces an error raised on the
insertBefore line that inserts the script tag. After
looking at
LAB.src.js I realised it was because the document isn't yet ready, which means we
need a way of deferring until appendTo.firstChild is
available.

The easiest way to do this is by using setTimeout:

(function(global) {
  var appendTo = document.head || document.getElementsByTagName('head');

  function require(scriptSrc, fn) {
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = scriptSrc;

    script.onload = script.onreadystatechange = function() {
      if (!script.readyState || (script.readyState === 'complete' || script.readyState === 'loaded')) {
        script.onload = script.onreadystatechange = null;
        fn();
        appendTo.removeChild(script);
      }
    };

    appendTo.insertBefore(script, appendTo.firstChild);
  }

  turing.require = function(scriptSrc, fn) {
    setTimeout(function() {
      if ('item' in appendTo) {
        if (!appendTo[0]) {
          return setTimeout(arguments.callee, 25);
        }

        appendTo = appendTo[0];
      }

      require(scriptSrc, fn);
    });
  };
}(window));

The callback will keep calling itself and returning until
appendTo[0] is available. At this point execution will
continue to call the original require function.

Conclusion

This example only illustrates loading scripts by inserting script tags.
There's no way to control the order the scripts are executed, and there
isn't any HTML5 support.

This code can be found in commit
2450ee3
.

References