Skip to content

Instantly share code, notes, and snippets.

@exogen
Last active October 8, 2015 21:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save exogen/90fec42135cdfaa62570 to your computer and use it in GitHub Desktop.
Save exogen/90fec42135cdfaa62570 to your computer and use it in GitHub Desktop.
Correct atomic-callback-on-execute async script loading
/**
* Script loading is difficult thanks to IE. We need callbacks to fire
* immediately following the script's execution, with no other scripts
* running in between. If other scripts on the page are able to run
* between our script and its callback, bad things can happen, such as
* `jQuery.noConflict` not being called in time, resulting in plugins
* latching onto our version of jQuery, etc.
*
* For IE<10 we use a relatively well-documented 'preloading' strategy,
* which ensures that the script is ready to execute *before* appending
* it to the DOM. That way when it is finally appended, it is
* executed immediately.
*
* References:
* 1. http://www.html5rocks.com/en/tutorials/speed/script-loading/
* 2. http://blog.getify.com/ie11-please-bring-real-script-preloading-back/
* 3. https://github.com/jrburke/requirejs/issues/526
* 4. https://connect.microsoft.com/IE/feedback/details/729164/ie10-dynamic-script-element-fires-loaded-readystate-prematurely
*/
var _pendingScripts = {},
_scriptCounter = 0;
function loadScript(src, callback, thisValue) {
thisValue = thisValue || this;
var script = document.createElement('script');
if (script.readyState && !('async' in script)) {
// This section is only for IE<10. Some other old browsers may
// satisfy the above condition and enter this branch, but we don't
// support those browsers anyway.
var id = _scriptCounter++,
isReady = { loaded: true, complete: true },
isComplete = { complete: true },
inserted = false,
done = false;
var stateChange = function() {
if (!inserted && isReady[script.readyState]) {
inserted = true;
// Get the first script element, we're just going to use it
// as a reference for where to insert ours. Do NOT try to do
// this just once at the top and then re-use the same script
// as a reference later. Some weird loaders *remove* script
// elements after the browser has executed their contents,
// so the same reference might not have a parentNode later.
var firstScript = document.getElementsByTagName('script')[0];
// Append the script to the DOM, triggering execution.
firstScript.parentNode.insertBefore(script, firstScript);
}
// It's possible for readyState to be 'complete' immediately
// after we insert (and execute) the script in the branch
// above. So check readyState again here and react without
// waiting for another onreadystatechange.
if (!done && isComplete[script.readyState]) {
done = true;
script.onreadystatechange = null;
_pendingScripts[id] = void 0;
if (callback) {
callback.call(thisValue);
}
}
};
// Attach the handler before setting src, otherwise we might
// miss events (consider that IE could fire them synchronously
// upon setting src, for example).
script.onreadystatechange = stateChange;
// Since we're not appending the script to the DOM yet, the
// reference to our script element might get garbage collected
// when this function ends, without onreadystatechange ever being
// fired. This has been witnessed to happen. Adding it to
// `_pendingScripts` ensures this can't happen.
_pendingScripts[id] = script;
// This triggers a request for the script, but its contents won't
// be executed until we append it to the DOM.
script.src = src;
// In some cases, the readyState is already 'loaded' immediately
// after setting src. It's a lie! Don't append to the DOM until
// the onreadystatechange event says so.
} else {
// This section is for modern browsers, including IE10+.
script.onload = function() {
script.onload = null;
if (callback) {
callback.call(thisValue);
}
};
script.async = true;
script.charset = 'utf-8';
script.src = src;
// Get the first script element, we're just going to use it
// as a reference for where to insert ours. Do NOT try to do
// this just once at the top and then re-use the same script
// as a reference later. Some weird loaders *remove* script
// elements after the browser has executed their contents,
// so the same reference might not have a parentNode later.
var firstScript = document.getElementsByTagName('script')[0];
firstScript.parentNode.insertBefore(script, firstScript);
}
};
@silentrob
Copy link

Super elegant!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment