Thursday, July 19, 2012

waitFor JavaScript

In the spirit of building the simplest thing that could possibly work, I revisit the concept in another of my posts: simple asynchronous JavaScript.

Time and time again I see web developers relying on the order that JavaScript loads in a page to ensure that the site will work. This keeps the mental model simple, "all the stuff above this code will be available here", but it means that people trying to speed up browsers and JavaScript interpreters have their hands tied. Why not instead load and execute as much JavaScript in parallel as possible, like a mutlithreaded application, and wait for your dependencies to load or preconditions to be met before your code runs.

You could do it all in just a few lines:

function waitFor(condition, callback) {
  function waiter(condition, callback) {
    return function() {
      var condMet = false;
      try {
        condMet = condition();
      } catch (e) {}

      if (condMet) {
        callback();
      } else {
        setTimeout(waiter(condition, callback), 5);
      }
    };
  }

  waiter(condition, callback)();
}

Using it looks a bit like this

waitFor(
  // condition
  function() {return window.dependency;},
  // callback
  function() {
    // do things using dependency.
  }
);

For example, here's how you might set this up to check if jQuery has been loaded and that a particular element on the page has been loaded in the DOM.

waitFor(function() {return $('#msgid');}, function() {
  $(document).ready(function(){
    $('#msgid').html('jQuery says "hi"!');
  });
});

Now it doesn't matter what order your script tags are in. Of course, you'll get the best performance if jQuery gets loaded before this waitFor call is made. The wait and retry loop adds some cost. If browsers had an event model available for script loading or conditions then we wouldn't have to poll using setTimeout/Interval. But for existing browsers all the way back to IE 6, this simple helper function will work.

For managing JavaScript dependencies there's also require.js which does quite a bit more (and is larger). I wrote this to be lighter weight and it is more general and flexible than a framework like require.js. Using waitFor, you could set up logic that is triggered if and when certain arbitrary conditions become true.

One little improvement, if you are waiting for lots of things then you would probably want to replace multiple timers with a single timer that will check all conditions. Like this:

function waitFor(condition, callback) {
  waitFor.waitingFor.push([condition, callback]);
  waitFor.check();
}

waitFor.waitingFor = [];

waitFor.check = function() {
  var stillWaitingFor = [];
  for (var i = 0; i < waitFor.waitingFor.length; i++) {
    var condMet = false;
    try {
      condMet = waitFor.waitingFor[i][0]();
    } catch (e) {}
    if (condMet) {
      waitFor.waitingFor[i][1]();
    } else {
      stillWaitingFor.push(waitFor.waitingFor[i]);
    }
  }

  waitFor.waitingFor = stillWaitingFor;
  if (stillWaitingFor.length > 0) {
    setTimeout(waitFor.check, 5);
  }
};

Here's a minified version. I added some newlines for formatting which you can take out.

function w(c,b){w.w.push([c,b]);w.c()}
w.w=[];w.c=function(){for(var s=[],b=0;b<w.w.length;b++)
{var d=!1;try{d=w.w[b][0]()}catch(e){}
if(d)w.w[b][1]();else s.push(w.w[b])}w.w=s;0<s.length
&&setTimeout(w.c,5)};var waitFor=w;

Happy coding!

No comments: