Asynchronous Resource Loading Part 6

2011-11-03 00:00:00 +0000 by Alex R. Young
*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/).

Previous parts of Asynchronous Resource Loading:

Handling Remote Scripts

In a world where XMLHttpRequest can load remote scripts,
asynchronous script loading would be relatively easy. However, as CDNs
are so popular loading third-party scripts is a common occurrence.

This tutorial takes the most basic technique for remote script loading,
script insertion, and demonstrates how to combine it with
XMLHttpRequest preloading.

Queueing and Preloading

Last week's tutorial showed how to load remote scripts using an
event-based queueing system. This must be modified to work with script
insertion. Fortunately, keeping this process event-based gives rise to a
simple algorithm:

  1. Preload all local scripts
  2. Once preloading is complete, emit an execute-next event
  3. If the script is preloaded, execute it. Afterwards, emit an
    execute-next event
  4. If the script is remote, fetch and execute it. Once execution is
    complete, emit an execute-next event

That gives rise to the following methods:

The callbacks for fetchExecute and execute
should emit execute-next. In fact, it should probably just
be one callback.


The tests are similar to the previous tests, but with the addition of a
remote script:

'test queue loading with remote in the middle': function() {
  var assertExpect = mixinExpect(assert);

  ]).on('complete', function() {
  }).on('loaded', function(item) {
    if (item.src === 'https://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js') {
      assertExpect.equal(typeof test13, 'undefined', 'test13 should not be set when remote is loaded');
      assertExpect.ok(window.swfobject, 'swfobject should be set');

    if (item.src === '/load-me.js?test12=12') {
      assertExpect.ok(!window.swfobject, 'swfobject should not be set when test12 is loaded');
      assertExpect.equal(typeof test13, 'undefined', 'test13 should not be set when test12 is loaded');
      assertExpect.equal(test12, 12, 'test12 should be set when test12 is loaded');

    if (item.src === '/load-me.js?test13=13') {
      assertExpect.equal(test13, 13);
      assertExpect.equal(test12, 12);

I've used the Google CDN for this test, and picked an arbitrary script
to load. Execution is tested by checking a global variable that we know
the remote script will set.


A counter is required to check how many scripts require preloaded, then
it gets decrement as each preload completes. I've called this
this.preloadCount in Queue, and the
preload event handler has been changed to emit
preload-complete when preloading finishes.

The only edge case is preloadAll must emit something when
there are no local scripts.

preloadAll: function() {
  var i, g, group, item, self = this;
  for (g = 0; g < this.groupKeys.length; g++) {
    group = this.groups[this.groupKeys[g]];

    for (i = 0; i < group.length; i++ ) {
      item = group[i];

      if (item.preload) {
        (function(groupItem) {
          requireWithXMLHttpRequest(groupItem.src, {}, function(script) {
            self.emit('preloaded', groupItem, script);

  if (this.preloadCount === 0) {

The preload event handler looks simpler than it did before:

installEventHandlers: function() {
  var self = this;

  this.on('preloaded', function(groupItem, options) {
    var group = self.groups[groupItem.group];
    groupItem.preloaded = true;
    groupItem.scriptOptions = options;

    if (self.preloadCount === 0) {

  this.on('preload-complete', function() {

This neatly encapsulates preloading and executing code, making the
distinction easier to follow. The next handler is the key one which
manages execution:

this.on('execute-next', function() {
  var groupItem = self.nextItem();

  function completeCallback() {
    groupItem.loaded = true;
    self.emit('loaded', groupItem);

  if (groupItem) {
    if (groupItem.preload) {
      self.execute(groupItem, completeCallback);
    } else {
      self.fetchExecute(groupItem, completeCallback);
  } else {

The key method here is really nextItem which finds an item
waiting to be executed, or fetch executed

nextItem: function() {
  var group, i, j, item;

  for (i = 0; i < this.groupKeys.length; i++) {
    group = this.groups[this.groupKeys[i]];
    for (j = 0; j < group.length; j++) {
      item = group[j];
      if (!item.loaded) {
        return item;

The execute code is retained from last week, but the group
completion handling code has been removed:

fetchExecute: function(item, fn) {
  var self = this;
  requireWithScriptInsertion(item.src, { async: true, defer: true }, function() {

execute: function(item, fn) {
  if (item && item.scriptOptions) {
    script = createScript(item.scriptOptions);


Both methods call the callback, which was set by the
execute-next handler. By using the same callback,
completeCallback, we remove the possibility of creating a
mistake in the completion behaviour -- it would be easy to forget to set
groupItem.loaded in one of the callbacks for example.


Although this doesn't use any preloading techniques for remote scripts,
it extends the event-based approach and better encapsulates execution
and preloading. I think you'll agree that creating remote script loaders
is not at all trivial, despite support from the HTML specifications.

If you want to read more about the gruesome details of script loading,
the comments on ControlJS Part
extremely interesting, because it shows some of the thinking that went
into LABjs. It also demonstrates an alternative HTML-oriented approach
that removes some of the headaches of doing everything purely in

This code can be found in commit