YepNope/Modernizr callbacks with global JavaScript variables and Internet Explorer

638 views Asked by At

Can anyone explain why, in Internet Explorer, code sample 1 doesn't work while code sample 2 works?

Code 1 (non-functional)

Modernizr.load([
    {
        load: [
            '../includes/css/foo.css',
            '../includes/js/foo.js',
            '../includes/js/bar.js'
        ],
        complete: function() {
            initBar();
        }
    }
]);

Code 2 (functional)

Modernizr.load([
    {
        load: [
            '../includes/css/foo.css',
            '../includes/js/foo.js',
            '../includes/js/bar.js'
        ],
        complete: function() {
            window.initBar();
        }
    }
]);

bar.js

var initBar = function() {
    // code here
};

It works fine in other browsers. I've tried moving the blocks to the head section as well as beneath the page. I've also tried wrapping the contents of the callback in $(document).ready(), but none have worked with code 1.

The error I am getting specifically is:

SCRIPT5009: « initBar » est indéfini

It is almost as if the callback is executed before the resources are finished loading, but if that was the case then why does code sample 2 work?

Also I will note that on refresh the page loads fine (most likely due to the fact that the resources are cached), but it also loads fine after clearing the cache. I have to restart my browser session after clearing the cache to reproduce the problem.

UPDATE: This problem extends to more than just functions. Any global variable defined in a JS file that is loaded doesn't seem to be accessible directly. It also occurs if I load the CSS at the top of the page rather than with the other resources asynchronously. In fact I'm also noticing this problem with some jQuery plugins that are loaded in this manner.

UPDATE 2: Here is the console.log() output as per debugging instructions below. I've changed bar to be an object instead of a function for the sake of illustrating this.

Internet Explorer:

   HTML1300: Une navigation s’est produite.
   Fichier : test18.php

   before .load() called
   before bar accessed
   typeof bar = undefined
   typeof window.bar = undefined

   SCRIPT5009: « bar » est indéfini
   Fichier : test18.js, ligne : 14, colonne : 13

   before bar defined

So it appears that the complete function executes before bar is defined. I find it strange that window.bar is also undefined yet works...

Firefox

[02:10:46,448] "before .load() called"
[02:10:47,184] "before bar defined"
[02:10:47,184] "before bar accessed"
[02:10:47,184] "typeof bar = object"
[02:10:47,184] "typeof window.bar = object"

Chrome

before .load() called
before bar defined
before bar accessed
typeof bar = object
typeof window.bar = object

Both Firefox and Chrome appear to be loading and executing the resources in the correct order.

1

There are 1 answers

10
jfriend00 On

First off, you should know that .load() in modernizr comes from the yepnope library so that's where you find the detailed documentation for it.

Here are the possible things that could be different in different browsers that I can think of:

  1. The exact timing of the loading of the scripts and thus the timing of when the complete() function gets called.

  2. The caching in the browser (which can affect the load timing).

  3. Because you are defining initBar by assigning it to a variable instead of a regular function initBar() definition, the function will not exist until that line of code executes whereas function initBar() will exist at script parse time.

  4. Make sure you version 1.5 or higher of the yepnope loading library (I don't know what modernizr version that corresponds to. The yepnope doc for .load() says this: "In versions of yepnope prior to 1.5 this [when the complete function is called] could vary from time to time".

  5. There are notes on this page that the yepnope library may not wait for .css files to load before calling the complete callback unless you have an add-in present. I don't know if that throws off the whole complete timing or what as I note that you do have .css files in your load list.

So, here's what I'd suggest to debug this:

1) Change your initBar definition to this:

function initBar() {
    // code here
}

2) Make sure your initBar definition is in the proper scope and reachable from your other code. Look out for things like being inside another function (onload, document.ready, etc...) which might make it unreachable.

3) Insert some console.log() statements like this to do some timing debugging:

console.log("before .load() called");
Modernizr.load([
    {
        load: [
            '../includes/css/foo.css',
            '../includes/js/foo.js',
            '../includes/js/bar.js'
        ],
        complete: function() {
            console.log("before initBar() called");
            console.log("typeof initBar = " + typeof initBar);
            console.log("typeof window.initBar = " + typeof window.initBar);
            initBar();
            console.log("after initBar() called");
        }
    }
]);


console.log("before initBar() defined");
function initBar() {
    // code here
}

Then, see what order things come out in and what the typeof statements say. The idea here is to try to figure out if things are executing in the wrong order or if a scope is wrong.

4) Try loading the .css file separately so it won't be affecting the .js loading.


Here's a replacement script that can load multiple scripts dynamically to replace the modernizr buggy .load() code. This one loads them all in parallel. This works only for scripts files (though the same concept could be used for .css files.

function loadScriptsInParallel(scripts, completeCallback) {
    var head = document.getElementsByTagName('head')[0];
    var remaining = scripts.length, i, scriptTag;

    function complete() {
        // make sure it's not called again for this script
        this.onreadystatechange = this.onload = function() {};
        // decrement remaining count and check if all are done
        --remaining;
        if (remaining === 0) {
            // all are done call the callback
            completeCallback();
        }
    }

    for (var i = 0; i < scripts.length; i++) {
        scriptTag = document.createElement('script');
        scriptTag.type = 'text/javascript';
        scriptTag.src = scripts[i];
        // most browsers
        scriptTag.onload = complete;
        // IE 6 & 7
        scriptTag.onreadystatechange = function() {
            if (this.readyState == 'complete') {
                complete.apply(this, arguments);
            }
        }
        head.appendChild(scriptTag);
    }
}

Sample usage:

loadScriptsInParallel([
    '../includes/js/foo.js',
    '../includes/js/bar.js'
], function() {
    // put code here for when all scripts are loaded
    initBar();
});

Working demo: http://jsfiddle.net/jfriend00/qs44R/

If you need them loaded sequentially (one after the other because of dependencies between them), then you could use this:

function loadScriptsInSequence(scripts, completeCallback) {
    var head = document.getElementsByTagName('head')[0];
    var remaining = scripts.length, i = 0;

    function loadNext() {
        var scriptTag = document.createElement('script');
        scriptTag.type = 'text/javascript';
        scriptTag.src = scripts[i++];
        // most browsers
        scriptTag.onload = complete;
        // IE 6 & 7
        scriptTag.onreadystatechange = function() {
            if (this.readyState == 'complete') {
                complete.apply(this, arguments);
            }
        }
        head.appendChild(scriptTag);
    }

    function complete() {
        // make sure it's not called again for this script
        this.onreadystatechange = this.onload = function() {};
        // decrement remaining count and check if all are done
        --remaining;
        if (remaining === 0) {
            // all are done call the callback
            completeCallback();
        } else {
            loadNext();
        }
    }

    loadNext();
}

Working demo: http://jsfiddle.net/jfriend00/9aVLW/