principlejavascriptMinor
Is this a good approach to loading JavaScript files asynchronously?
Viewed 0 times
thisjavascriptfilesloadingasynchronouslygoodapproach
Problem
I want to load JS files asynchronously to speed up page loading, but also need to execute JS code only once the scripts finish loading (so I need a way to implement callbacks).
I based this off of some example code on the Google Pagespeed website:
I know there are libraries out there for dependency management, callback functions, etc., but I just want something very lightweight and using a library would mean having another HTTP request.
I'm very new to JavaScript and would really appreciate any feedback or advice you might have!
I based this off of some example code on the Google Pagespeed website:
// Add script elements as a children of the body
function downloadJSAtOnload() {
var filesToLoad = [
["/assets/plugins/typeahead.bundle.min.js", "onTypeaheadLoaded"],
["https://apis.google.com/js/client:plusone.js?onload=googlePlusOnloadCallback", ""]
];
filesToLoad.forEach(function(entry) {
var element = document.createElement("script");
element.src = entry[0];
if (entry[1] != "") { // if an onload callback is present (NOTE: DOES NOT SUPPORT NAMESPACES -- http://stackoverflow.com/a/359910/1101095)
element.onload = function() {
if (typeof window[entry[1]] != "undefined") {
window[entry[1]]();
}
};
}
document.body.appendChild(element);
});
}
// Check for browser support of event handling capability
if (window.addEventListener)
window.addEventListener("load", downloadJSAtOnload, false);
else if (window.attachEvent)
window.attachEvent("onload", downloadJSAtOnload);
else window.onload = downloadJSAtOnload;I know there are libraries out there for dependency management, callback functions, etc., but I just want something very lightweight and using a library would mean having another HTTP request.
I'm very new to JavaScript and would really appreciate any feedback or advice you might have!
Solution
First of all, I would split you problem into 3 parts:
These are 3 independent parts, which might be reused for other purposes, if seperated correctly.
You're using 2 strings to specify a file to be loaded. But instead of writing
you can as well also pass the actual function:
This not only ensures that the function actually exists, it also gives you the opportunity to use anonymous functions:
However, speaking of segregation of responsibilities, you should write a function on its own, without the need of an Array anymore:
This function looks much clearer, and you can see at first glance what it is doing.
Note that I checked the existence of the callback before I append an onload-attribute. This is a bit more efficient than your code, which always calls the
You might also want to add the
If you only have to load a single script, you can call this function directly. No need to generate useless Arrays.
If you only want to load several files, and calling a callback after each file, this is an easy task, using your original array (with functions instead of strings):
And that's all.
However, if you want to have one callback-function which is called after all files have been loaded, this gets a bit tricky. You don't know which file is the last one, and which file should trigger the actual callback-function.
This is how the function is being called:
This function loads 2 files simultaneously, and calls the callback after the last one has been loaded. Afterwards it prints which files have been loaded successfully, and which loading-operation failed.
This only works, if the initial
Another advantage is, that you can extend this function to also load other files like images, json, xml and so on.
- Loading a js-file asynchronously, and calling a callback-function
- Loading several js-files an once asynchronously
- Using an onload-Handler, which triggers everything
These are 3 independent parts, which might be reused for other purposes, if seperated correctly.
- Loading a js-file asynchronously
You're using 2 strings to specify a file to be loaded. But instead of writing
["file.js", "cbFunction"]you can as well also pass the actual function:
["file.js", cbFunction]This not only ensures that the function actually exists, it also gives you the opportunity to use anonymous functions:
["file.js", function(){ alert("loaded!"); } ]However, speaking of segregation of responsibilities, you should write a function on its own, without the need of an Array anymore:
function loadScript(path, scriptLoadedCallback){
var element=document.createElement('script'); //generate -tag
element.setAttribute("type","text/javascript");
element.setAttribute("src", path);
if(typeof(scriptLoadedCallback) == 'function'){ //makes the callback-function optional
element.onload = function() {
return scriptLoadedCallback(true, path); //true = successfull; the path is needed later
};
element.onerror = function() { //you might also call the cb on error
this.parentNode.removeChild(this); //remove faulty node from DOM
return scriptLoadedCallback(false, path); //false = error; the path is needed later
};
}
document.head.appendChild(fileref); //insert the node in DOM (end of ), and load the script
}This function looks much clearer, and you can see at first glance what it is doing.
Note that I checked the existence of the callback before I append an onload-attribute. This is a bit more efficient than your code, which always calls the
onload-Handler and then does nothing.You might also want to add the
onerror-method, otherwise you will never get notified if the loading failed (This usually happens if the js-file doesn't exist).If you only have to load a single script, you can call this function directly. No need to generate useless Arrays.
- Loading several js-files an once
If you only want to load several files, and calling a callback after each file, this is an easy task, using your original array (with functions instead of strings):
var filesToLoad = [
["file1.js", cbFunctionToCall],
["file2.js"]
];
for(var i=0; i<filesToLoad.length; i++){ //I prefer a for-loop because its more readable
loadScript(filesToLoad[i][0], filesToLoad[i][1];
}And that's all.
However, if you want to have one callback-function which is called after all files have been loaded, this gets a bit tricky. You don't know which file is the last one, and which file should trigger the actual callback-function.
function loadScriptArray(contentArray, contentLoadedCallback){
var contentQuantity = contentArray.length; //Number of Files that needs to be loaded
var contentCompleted = 0; //Number of Files, that have already been loaded
var returnParamList = {}; //List with return-Parameters
if(contentQuantity == 0){ //We don't have anything to load
return contentLoadedCallback({});
}
for (var i = 0; i < contentQuantity; i++) {
loadScript(contentArray[i], function(success, path){ //This anonymous function is called everytime a script is finished
//The only way to know which script finished, is to pass the path as an parameter
returnParamList[ identifier ] = returnParam; //store the returnValue (true=success, false=error)
contentCompleted++;
if(contentCompleted == contentQuantity){ //this was the last file
if(typeof contentLoadedCallback== 'function'){
contentLoadedCallback(returnParamList);
}
}
});
}
}This is how the function is being called:
loadScriptArray(["file1.js", "file2.js"], function(returnParamList){
alert("All Scripts finished. \n"+
"File1: "+returnParamList["file1.js"]+"\n"+
"File2: "+returnParamList["file2.js"]);
});This function loads 2 files simultaneously, and calls the callback after the last one has been loaded. Afterwards it prints which files have been loaded successfully, and which loading-operation failed.
This only works, if the initial
loadScript()-Function always calls the callback-Function - both if it succeeded or failed. And to report the success back to the caller, the callback-function also needs the filePath as an parameter.Another advantage is, that you can extend this function to also load other files like images, json, xml and so on.
Code Snippets
["file.js", "cbFunction"]["file.js", cbFunction]["file.js", function(){ alert("loaded!"); } ]function loadScript(path, scriptLoadedCallback){
var element=document.createElement('script'); //generate <script>-tag
element.setAttribute("type","text/javascript");
element.setAttribute("src", path);
if(typeof(scriptLoadedCallback) == 'function'){ //makes the callback-function optional
element.onload = function() {
return scriptLoadedCallback(true, path); //true = successfull; the path is needed later
};
element.onerror = function() { //you might also call the cb on error
this.parentNode.removeChild(this); //remove faulty node from DOM
return scriptLoadedCallback(false, path); //false = error; the path is needed later
};
}
document.head.appendChild(fileref); //insert the node in DOM (end of <head>), and load the script
}var filesToLoad = [
["file1.js", cbFunctionToCall],
["file2.js"]
];
for(var i=0; i<filesToLoad.length; i++){ //I prefer a for-loop because its more readable
loadScript(filesToLoad[i][0], filesToLoad[i][1];
}Context
StackExchange Code Review Q#57458, answer score: 5
Revisions (0)
No revisions yet.