Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Load javascript async, then check DOM loaded before executing callback

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]

like image 912
Ken Avatar asked Nov 22 '10 18:11

Ken


1 Answers

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");
   }
);
like image 123
25 revs, 4 users 83% Avatar answered Oct 10 '22 12:10

25 revs, 4 users 83%