10

Problem:
Load js files asynchronously, then check to see if the dom is loaded before the callback from loading the files is executed.

edit: We do not use jQuery; we use Prototype.
edit: added more comments to the code example.

I am trying to load all of my js files asynchronously so as to keep them from blocking the rest of the page. But when the scripts load and the callback is called, I need to know if the DOM has been loaded or not, so I know how to structure the callback. See below:

//load asynchronously
(function(){
        var e = document.createElement('script'); 
        e.type = "text/javascript";
        e.async = true;
        e.src = srcstr; 
        // a little magic to make the callback happen
        if(navigator.userAgent.indexOf("Opera")){
            e.text = "initPage();";
        }else if(navigator.userAgent.indexOf("MSIE")){
            e.onreadystatechange = initPage;
        }else{
            e.innerHTML = "initPage();";
        }
        // attach the file to the document
        document.getElementsByTagName('head')[0].appendChild(e);
})();

initPageHelper = function(){ 
    //requires DOM be loaded
}

initPage = function(){
    if(domLoaded){ // if dom is already loaded, just call the function
        initPageHelper();
    }else{ //if dom is not loaded, attach the function to be run when it does load
        document.observe("dom:loaded", initPageHelper);
    }
}

The callback gets called properly due to some magic behind the scenes that you can learn about from this Google talk: http://www.youtube.com/watch?v=52gL93S3usU&feature=related

What's the easiest, cross-browser method for asking if the DOM has loaded already?

EDIT
Here's the full solution I went with.
I included prototype and the asynchronous script loader using the normal method. Life is just so much easier with prototype, so I'm willing to block for that script.

<script type="text/javascript" src="prototype/prototype.js"></script>
<script type="text/javascript" src="asyncLoader.js"></script>

And actually, in my code I minified the two files above and put them together into one file to minimize transfer time and http requests.

Then I define what I want to run when the DOM loads, and then call the function to load the other scripts.

<script type="text/javascript">
    initPage = function(){
    ...
    }
</script>
<script type="text/javascript">
    loadScriptAsync("scriptaculous/scriptaculous.js", initPage);
    loadScriptAsync("scriptaculous/effects.js", initPage);
    loadScriptAsync("scriptaculous/controls.js", initPage);
        ...
    loadScriptAsync("mypage.js", initPage);
</script>

Likewise, the requests above are actually compressed into one httpRequest using a minifier. They are left separate here for readability. There is a snippet at the bottom of this post showing what the code looks like with the minifier.

The code for asyncLoader.js is the following:

/**
 * Allows you to load js files asynchronously, with a callback that can be 
 * called immediately after the script loads, OR after the script loads and 
 * after the DOM is loaded. 
 * 
 * Prototype.js must be loaded first. 
 * 
 * For best results, create a regular script tag that calls a minified, combined
 * file that contains Prototype.js, and this file. Then all subsequent scripts
 * should be loaded using this function. 
 * 
 */
var onload_queue = [];
var dom_loaded = false;
function loadScriptAsync(src, callback, run_immediately) {
      var script = document.createElement('script'); 
      script.type = "text/javascript";
      script.async = true;
      script.src = src;
      if("undefined" != typeof callback){
          script.onload = function() {
                if (dom_loaded || run_immediately) 
                  callback();
                else 
                  onload_queue.push(callback);
                // clean up for IE and Opera
                script.onload = null;
                script.onreadystatechange = null;
          };

          script.onreadystatechange = function() {
            if (script.readyState == 'complete'){
                if (dom_loaded || run_immediately) 
                  callback();
                else 
                  onload_queue.push(callback);
                // clean up for IE and Opera
                script.onload = null;
                script.onreadystatechange = null;
            }else if(script.readyState == 'loaded'){
                eval(script);
                 if (dom_loaded || run_immediately) 
                      callback();
                else 
                  onload_queue.push(callback);
                // clean up for IE and Opera
                script.onload = null;
                script.onreadystatechange = null;
            }
          };
      }
      var head = document.getElementsByTagName('head')[0];
      head.appendChild(script);
}
document.observe("dom:loaded", function(){
    dom_loaded = true;
    var len = onload_queue.length;
    for (var i = 0; i < len; i++) {
        onload_queue[i]();
    }
    onload_queue = null;
});

I added the option to run a script immediately, if you have scripts that don't rely on the page DOM being fully loaded.

The minified requests actually look like:

<script type="text/javascript" src="/min/?b=javascript/lib&f=prototype/prototype.js,asyncLoader.js"></script>
<script type="text/javascript"> initPage = function(e){...}</script>
<script type="text/javascript">
    srcstr = "/min/?f=<?=implode(',', $js_files)?>";
    loadScriptAsync(srcstr, initPage);
 </script>

They are using the plugin from: [http://code.google.com/p/minify/][1]

peterh
  • 11,875
  • 18
  • 85
  • 108
Ken
  • 672
  • 1
  • 6
  • 14

2 Answers2

8

What you need is a simple queue of onload functions. Also please avoid browser sniffing as it is unstable and not future proof. For full source code see the [Demo]

var onload_queue = [];
var dom_loaded = false;

function loadScriptAsync(src, callback) {
  var script = document.createElement('script'); 
  script.type = "text/javascript";
  script.async = true;
  script.src = src;
  script.onload = script.onreadystatechange = function() {
    if (dom_loaded) 
      callback();
    else 
      onload_queue.push(callback);
    // clean up for IE and Opera
    script.onload = null;
    script.onreadystatechange = null;
  };
  var head = document.getElementsByTagName('head')[0];
  head.appendChild(script);
}

function domLoaded() {
   dom_loaded = true;
   var len = onload_queue.length;
   for (var i = 0; i < len; i++) {
     onload_queue[i]();
   }
   onload_queue = null;
};

// Dean's dom:loaded code goes here
// do stuff
domLoaded();

Test usage

loadScriptAsync(
  "http://code.jquery.com/jquery-1.4.4.js", 
  function() {
      alert("script has been loaded");
   }
);
gblazex
  • 49,155
  • 12
  • 98
  • 91
  • but what if the async callback returns after the dom has loaded? – Ken Nov 22 '10 at 19:28
  • the only problem with this update is that I also wanted to load the prototype library asynchronously, which means that document.observe() will not be available. – Ken Nov 22 '10 at 19:46
  • that's no problem, just use http://dean.edwards.name/weblog/2006/06/again/ for `dom:loaded`, even jQuery uses a modified version of this. – gblazex Nov 22 '10 at 19:48
  • script.onload = script.onreadystatechange = function()... this function will be called twice in Opera. you have to set a flag and check that the function has not been run yet. – Ken Nov 22 '10 at 19:49
  • 1
    Actaully you don't have to. Just remove the event listeners by **nulling** them. It also prevents IE memory leaks! Updated the code + demo. – gblazex Nov 22 '10 at 19:59
-1

You can always put your initial loader script at the bottom, right before the closing body tag.

rob
  • 9,933
  • 7
  • 42
  • 73
  • this won't work, as i want my scripts to start loading asynchronously at the top of the page – Ken Nov 22 '10 at 19:04
  • well, at the bottom you can add a script tag that simply sets a global variable "var loaded=true;" or the like. Or it could have an element with an id, and you can check for that element existing. – rob Nov 22 '10 at 19:25
  • And why do you want them to load at the top? If you just place them at the bottom, the parser will go over the html, load the images non-blocking and will then struggle with your javascripts while still showing the rest of the page. And as it seems, that your scripts need a loaded dom anyway, why fiddling with that? A simple $(document).observe("dom:loaded", function() {...}) will do the work. – philgiese Nov 22 '10 at 19:52
  • Because once the dom is loaded, pieces of it can be displayed. I need the controls to be maximally responsive, and many of them are in js. So I want the js to transfer asynchronously at the same time as the markup, then i want the handlers attached as soon as the dom is ready to create maximum responsiveness. I don't want to wait to transfer the files until after the dom is loaded, and then attach the handlers. – Ken Nov 22 '10 at 20:01
  • Makes sense, although I doubt you really buy anything by having the load initiated at the top of the page vs. the bottom (unless the page is huge and slow to generate). Regardless, you can do it by calling a function at the bottom of the page, and call the same function at the bottom of each javascript file that is loaded. Only the very last time the function is called will it actually trigger anything....all the other times it will be keeping a count. So it doesn't matter if the dom is loaded first, or the js files....the last time through it will trigger whatever behavior you need. – rob Nov 22 '10 at 20:14
  • this would not wait on the async scripts to finish loading. It would just be a sketchy proxy for DOMContentLoaded. – Kyle Baker Apr 07 '17 at 16:17