4

NOT A DUPLICATE AS I HAVE YET TO FOUND A SATISFYING ANSWER ON OTHER THREADS:


Looking for native Javascript answers, no jQuery, no requireJS, and so forth please :)


SUMMARY OF THE ENTIRE QUESTION:

I want to asynchronously load scripts but have ordered execution

I am trying to enforce that the code in the inserted script elements execute exactly in the same order as they were added to the dom tree.

That is, if I insert two script tags, first and second, any code in first must fire before the second, no matter who finishes loading first.

I have tried with the async attribute and defer attribute when inserting into the head but doesn't seem to obey.

I have tried with element.setAttribute("defer", "") and element.setAttribute("async", false) and other combinations.

The issue I am experiencing currently has to do when including an external script, but that is also the only test I have performed where there is latency.

The second script, which is a local one is always fired before the first one, even though it is inserted afterwards in the dom tree ( head ).

A) Note that I am still trying to insert both script elements into the DOM. Ofcourse the above could be achieved by inserting first, let it finish and insert the second one, but I was hoping there would be another way because this might be slow.

My understanding is that RequireJS seems to be doing just this, so it should be possible. However, requireJS might be pulling it off by doing it as described in A).

Code if you would like to try directly in firebug, just copy and paste:

    function loadScript(path, callback, errorCallback, options) {
            var element = document.createElement('script');
            element.setAttribute("type", 'text/javascript');
            element.setAttribute("src", path);

            return loadElement(element, callback, errorCallback, options);
    }

    function loadElement(element, callback, errorCallback, options) {
            element.setAttribute("defer", "");
            // element.setAttribute("async", "false");

            element.loaded = false;

            if (element.readyState){  // IE
                    element.onreadystatechange = function(){
                            if (element.readyState == "loaded" || element.readyState == "complete"){
                                    element.onreadystatechange = null;

                                    loadElementOnLoad(element, callback);
                            }
                    };
            } else {                 // Others
                    element.onload = function() {
                            loadElementOnLoad(element, callback);
                    };
            }

            element.onerror = function() {
                    errorCallback && errorCallback(element);
            };

            (document.head || document.getElementsByTagName('head')[0] || document.body).appendChild(element);

            return element;
    }

    function loadElementOnLoad(element, callback) {
            if (element.loaded != true) {
                    element.loaded = true;
                    if ( callback ) callback(element);
            }
    }




loadScript("http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js",function() {
  alert(1);  
})

loadScript("http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",function() {
  alert(2);  
})

If you try the above code in like firebug, most often it will fire 2, and then 1. I want to ensure 1 and then 2 but include both in the head.

Community
  • 1
  • 1
mjs
  • 21,431
  • 31
  • 118
  • 200
  • @Asad as mentioned in the question, I am trying to avoid this way of loading it, since network loading of the scripts should still be loaded async. Perhaps not possible though ... – mjs Jul 31 '13 at 18:43
  • **"Ofcourse the above could be achieved by inserting first, let it finish and insert the second one, but I was hoping there would be another way because this might be slow"**. That would be no slower than inserting both scripts at the same time and somehow making the second one wait before it loaded (which AFAIK isn't even possible). Could you please clarify what performance difference you're talking about? – Asad Saeeduddin Jul 31 '13 at 19:00
  • 1
    @Asad There is a difference. You see, if you load 10 scripts at the same time, all 10 from different hosts, whereas two hosts are really really slow, taking 20 and 30 seconds each say, it will for sure take MORE than 50 seconds to load all of them. But if the code is fetched concurrently, starting 10 threads, you might be able to pull it off in 30 seconds, that is all others finishes, but still waiting for the slowest one, making it a total of 30 seconds to load all 10 instead of 50 seconds. See the difference? – mjs Jul 31 '13 at 19:05
  • I've had another go at the solution. Does [this demo](http://jsfiddle.net/5xjrm/) do what you were looking for? – Asad Saeeduddin Jul 31 '13 at 21:29
  • @Asad I cannot see where this actually includes a script. It seems to write the content to the iframe. I have thought about the iframe and using its header and it might work if your example is further worked on. However, currently there is still some work to be done. You need to actually insert the scripts to the dom, not their content. Their content is only available within that iframe and the iframe has its own window object I believe. But things can be sent upwards. Perhaps if loading the script in the head will ensure right execution order, but unsure how globals will be handled though. – mjs Jul 31 '13 at 21:41
  • 1
    I forgot to actually append the script elements to the head. Here is a demo that does this: http://jsfiddle.net/Cucbq/ – Asad Saeeduddin Jul 31 '13 at 21:55
  • @Asad I just tested it, unfortunately I don't think it still working. I think it is the right way, but right now you are simply executing console.log("executed" + ) right after it is inserted. Try logging the actual objects and you will see that they are not set. See here: http://snag.gy/PHVyp.jpg I should also mention I have now come up with an own solution that I will be posting any minute. I believe that it works. – mjs Jul 31 '13 at 22:11
  • I think you were mistaken there. The scripts are being loaded fine, the problem is that in the JSFiddle code editor, the scripts are loaded into the iframe on the right, and therefore their variables are not in scope in the main frame. Try your test on the `/show` page here: http://jsfiddle.net/Cucbq/show/ – Asad Saeeduddin Jul 31 '13 at 23:35
  • I ran the code in firebug. As you can see in my image, they will be present 2000 milliseconds later. But not when you are logging executed ... – mjs Jul 31 '13 at 23:43
  • I have been trying with iframes now for a while, looks like there won't be any callbacks, but they probably get loaded in the right order though... but no callback :( See new question to follow : http://stackoverflow.com/questions/17983392/javascript-iframe-without-src-with-nested-scripts – mjs Aug 01 '13 at 00:43
  • I've changed the code a bit to give you a callback when the script has been evaluated. In my earlier example it wasn't actually loading from the cache, because jQuery was adding a redundant timestamp when inserting the script into the head. With [this example](http://jsfiddle.net/U89uN/show/) (in which it actually loads from the cache), it takes 150ms on average to evaluate the cached scripts. The scripts are only requested once. – Asad Saeeduddin Aug 01 '13 at 01:35
  • So, does it work now according to you? – mjs Aug 01 '13 at 10:31

5 Answers5

2

if I insert two script tags, first and second, any code in first must fire before the second, no matter who finishes loading first. I have tried with the async attribute and defer attribute

No, async and defer won't help you here. Whenever you dynamically insert script elements into the DOM, they are loaded and executed asynchronically. You can't do anything against that.

My understanding is that RequireJS seems to be doing just this

No. Even with RequireJS the scripts are executed asynchronous and not in order. Only the module initialiser functions in those scripts are just define()d, not executed. Requirejs then does look when their dependencies are met and executes them later when the other modules are loaded.

Of course you can reinvent the wheel, but you will have to go with a requirejs-like structure.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Bergi, I would kill ( joking ) to figure out how requirejs is doing this! I am building my own AMD like library ( I am almost done, this is the only thing left ). I have a define and require method of my own. The thing is that even if you have your own define method, how do you connect the callback in that method with the right script element? define would somehow have to figure out what the script was that invoked the method was. Only firefox have a document.currentScript property, if that is the method used by requirejs, how is it solving it for other browsers. Can we chat?? Please :) – mjs Jul 31 '13 at 19:56
  • That's the only thing I don't understand at requireJS as well :-/ I guess it's doing some magic in the onload handler for the script, but I have had no time to read the source yet. Personally I prefer just passing the module name as a string to `define`… – Bergi Jul 31 '13 at 19:59
  • Bergi, don't give up on me man :) I just ran into this issue today, expecting the head stuff to behave as expected. I am building some serious revolutionary stuff here, not just this AMD module. It is compromised by four libraries, including a template engine ala grails. I think you would be impressed. Perhaps you would like a peak, and perhaps become one of the contributors? :) It is very neat and easy to understand code ( with little guidance ). I am ready to almost release all of it to the public. It would be an honour having you onboard :) – mjs Jul 31 '13 at 20:03
  • Regarding passing the name, yes, I understand, I currently support that, but unfortunately I don't want to be worse than requrejs :( The code in requirejs is hard to figure out. – mjs Jul 31 '13 at 20:06
  • Bergi, I believe I have now solved the main issue here... See my answer below which is only for this question alone but my usage follows the same principle. The trick is that when a method call is called such as define, all arguments are put on stack, and nothing else is executed. Then, when that method call is done, in all my tests so far, and I believe this is true; the next call is the callback to the loadScript method. You pop the stack, and it should be the one just pushed. – mjs Jul 31 '13 at 22:23
  • In the callback you now know the element, and you have your arguments to forward the call on. Not sure this is how requirejs does it, but my lib will :) – mjs Jul 31 '13 at 22:25
  • 1
    I've read the requirejs source now and it's actually quite simple. See [this part](https://github.com/jrburke/requirejs/blob/master/require.js#L2013) where `define` gets the current module name to store it on the queue, the [`getInteractiveScript` function](https://github.com/jrburke/requirejs/blob/master/require.js#L1907) and the [`load` method](https://github.com/jrburke/requirejs/blob/master/require.js#L1810) for scripts. – Bergi Jul 31 '13 at 23:00
2

Ok, I think I have now came up with a solution.

The trick is that we keep track of each script to be loaded and their order as we insert them into the dom tree. Each of their callback is then registered accordingly to their element.

Then we keep track of when all has finished loading and when they all have, we go through the stack and fire their callbacks.

var stack = [];
stack.loaded = 0;
function loadScriptNew(path, callback) {
        var o = { callback: callback };
        stack.push(o);

        loadScript(path, function() {
                o.callbackArgs = arguments;
                stack.loaded++;
                executeWhenReady();
        });
}

function executeWhenReady() {
        if ( stack.length == stack.loaded ) {
                while(stack.length) {
                        var o = stack.pop();
                        o.callback.apply(undefined, o.callbackArgs);
                }

                stack.loaded = 0;
        }
}

// The above is what has been added to the code in the question.

function loadScript(path, callback) {
        var element = document.createElement('script');
        element.setAttribute("type", 'text/javascript');
        element.setAttribute("src", path);

        return loadElement(element, callback);
}

function loadElement(element, callback) {
        element.setAttribute("defer", "");
        // element.setAttribute("async", "false");

        element.loaded = false;

        if (element.readyState){  // IE
                element.onreadystatechange = function(){
                        if (element.readyState == "loaded" || element.readyState == "complete"){
                                element.onreadystatechange = null;

                                loadElementOnLoad(element, callback);
                        }
                };
        } else {                 // Others
                element.onload = function() {
                        loadElementOnLoad(element, callback);
                };
        }

        (document.head || document.getElementsByTagName('head')[0] || document.body).appendChild(element);

        return element;
}

function loadElementOnLoad(element, callback) {
        if (element.loaded != true) {
                element.loaded = true;
                if ( callback ) callback(element);
        }
}

loadScriptNew("http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.js",function() {
        alert(1);
});

loadScriptNew("http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",function() {
        alert(2);
});

Ok, some of you might argue that there is missing info in the question, which I will give you and here we are actually just solving the callback. You are right. The code in the script is still executed in the wrong order, but the callback is now.

But for me this is good enough, as I intend to wrap all code that is loaded in a method call, alá AMD, such as a require or define call and will put on stack there, and then fire them in the callback instead.

I am still hoping out for Asad and his iframe solution, which I believe might provide the best answer to this question. For me though, this solution will solve my problems :)

mjs
  • 21,431
  • 31
  • 118
  • 200
  • It's a queue, not a stack (you `shift` out of it instead from `pop`ing off it)! And to me this just ensures that callback 2 is called after callback 1, not that script 2 is executed after script 1. Maybe I misunderstood your question… – Bergi Jul 31 '13 at 22:35
  • Two mistakes in `executeWhenReady`: It needs to be `o.callback.apply(null, o.callbackArgs);`, and I'm missing a `stack.loaded = 0;` reset – Bergi Jul 31 '13 at 22:38
  • 1
    Will you have control over all the scripts that you are loading? That is, can you add code to the scripts being loaded? For example for angular JS, is it feasible to add some wrapping code and upload this modified version to a CDN? – Asad Saeeduddin Jul 31 '13 at 22:40
  • @Bergi to your first comment, yes I do mention that this is a bit different from my question, but since now know that I have a wrapper call, this will do just fine for me. Yes, I see that I am shifting here, it should pop, and I have missed the undefined. You are observant. My real solution doesn't contain this code really, this was just created for this question :) – mjs Jul 31 '13 at 23:16
  • @Asad Yes, it will be possible. They will have require and define methods wrapping the calls that I have control over. – mjs Jul 31 '13 at 23:20
1

After a while of fiddling around with it, here is what I came up with. Requests for the scripts are sent off immediately, but they are executed only in a specified order.

The algorithm:

The algorithm is to maintain a tree (I didn't have time to implement this: right now it is just the degenerate case of a list) of scripts that need to be executed. Requests for all of these are dispatched nearly simultaneously. Every time a script is loaded, two things happen: 1) the script is added to a flat list of loaded scripts, and 2) going down from the root node, as many scripts in each branch that are loaded but have not been executed are executed.

The cool thing about this is that not all scripts need to be loaded in order for execution to begin.

The implementation:

For demonstration purposes, I am iterating backward over the scriptsToExecute array, so that the request for CFInstall is sent off before the request for angularJS. This does not necessarily mean CFInstall will load before angularJS, but there is a better chance of it happening. Regardless of this, angularJS will always be evaluated before CFInstall.

Note that I've used jQuery to make my life easier as far as creating the iframe element and assigning the load handler is concerned, but you can write this without jQuery:

// The array of scripts to load and execute

var scriptsToExecute = [
    "http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js?t=" + Date.now(), 
    "http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js?t=" + Date.now()
];


// Loaded scripts are stored here

var loadedScripts = {};


// For demonstration purposes, the requests are sent in reverse order.
// They will still be executed in the order specified in the array.

(function start() {
    for (var i = scriptsToExecute.length - 1; i >= 0; i--) {
        (function () {
            var addr = scriptsToExecute[i];
            requestData(addr, function () {
                console.log("loaded " + addr);
            });
        })();
    }
})();


// This function executes as many scripts as it currently can, by
// inserting script tags with the corresponding src attribute. The
// scripts aren't reloaded, since they are in the cache. You could
// alternatively eval `script.code`

function executeScript(script) {
    loadedScripts[script.URL] = script.code

    while (loadedScripts.hasOwnProperty(scriptsToExecute[0])) {
        var scriptToRun = scriptsToExecute.shift()
        var element = document.createElement('script');
        element.setAttribute("type", 'text/javascript');
        element.setAttribute("src", scriptToRun);

        $('head').append(element);

        console.log("executed " + scriptToRun);
    }
}


// This function fires off a request for a script

function requestData(path, loadCallback) {
    var iframe = $("<iframe/>").load(function () {
        loadCallback();
        executeScript({
            URL: $(this).attr("src"),
            code: $(this).html()
        });
    }).attr({"src" : path, "display" : "none"}).appendTo($('body'));
}

You can see a demo here. Observe the console.

Asad Saeeduddin
  • 46,193
  • 6
  • 90
  • 139
  • So you expect the scripts to be cached and then executed synchronously when add as a ` – Bergi Jul 31 '13 at 22:08
  • @Bergi I can eval the scripts instead of adding them as ` – Asad Saeeduddin Jul 31 '13 at 22:11
1

I am posting here just like a draft
This do not work because cross-domain police
Here the idea is to obtain all scripts first and when they are in memory, execute them in order.

function loadScript(order, path) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET",path,true);
    xhr.send();
    xhr.onreadystatechange = function(){
        if(xhr.readyState  == 4){
            if(xhr.status >= 200 && xhr.status < 300 || xhr == 304){
                loadedScripts[order] = xhr.responseText;
            }
            else {
                //deal with error
                loadedScripts[order] = 'alert("this is a failure to load script '+order+'");';
                // or  loadedScripts[order] = '';  // this smoothly fails
            }
            alert(order+' - '+xhr.status+' > '+xhr.responseText);                // this is to show the completion order.  Careful, FF stacks aletrs so you see in reverse.
            // am I the last one ???
            executeAllScripts();
        }
    };
}

function executeAllScripts(){
    if(loadedScripts.length!=scriptsToLoad.length) return;
    for(var a=0; a<loadedScripts.length; a++) eval(loadedScripts[a]);
    scriptsToLoad = [];
}


var loadedScripts = [];
var scriptsToLoad = [
   "http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js",
   "http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",
   "http://nowhere.existing.real_script.com.ar/return404.js"
];

// load all   even in reverse order ... or randomly
for(var a=0; a<scriptsToLoad.length; a++) loadScript(a, scriptsToLoad[a]);
Saic Siquot
  • 6,513
  • 5
  • 34
  • 56
  • Unfortunately as I you mention this will not work cross-site. Usage of eval could also be avoided by creating a new script tag without src but with the code to fire them immediately instead of eval – mjs Jul 31 '13 at 22:27
  • 1
    This was posted, just to help. As I see, I glad it helped, as your solution uses same queue idea. I also wait as you for a complete solution, just for the knowledge. On any news, please let me know. :) – Saic Siquot Jul 31 '13 at 22:35
  • Yes, all ideas are useful :) I will let you know. However, constructing an iframe should work, just not in my interest in writing up that solution myself as I have something that works right now. Iframe solution in my case would be hack compared to this one though. – mjs Jul 31 '13 at 23:12
0

cant you nest the loading using ur callbacks?

ie:

loadScript("http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js",function() {
    alert(1);
    loadScript("http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",function() {
        alert(2);  
    })
})
darmce
  • 41
  • 2
  • 1
    as mentioned in the question, I am trying to avoid this way of loading it, since network loading of the scripts should still be loaded async. Perhaps not possible though . – mjs Jul 31 '13 at 18:44
  • 1
    @Hamidam "Since network loading of the scripts should still be loaded async": That doesn't make any sense. "Loading" itself is always asynchronous. No one has control over the rate at which the server sends you packets. If you pass the option for synchronous loading to the XHR wrapper, all it does is block until loading is finished. That is the same as this approach. – Asad Saeeduddin Jul 31 '13 at 18:46
  • 1
    yeah, i think you need javascript to control ordering (ie requirejs or any AMD). placing in the head will not achieve this, and is the reason for AMDs – darmce Jul 31 '13 at 18:46
  • @Asad But the proposed way is synchronous, you will only load one script at a time since the second script is added to the header after the other has finished. – mjs Jul 31 '13 at 18:48
  • @darmce But I want to know how requireJS does this then. How are they doing that? Are they loading it one script at a time like you are proposing? – mjs Jul 31 '13 at 18:49
  • @Hamidam That is what happens in the approach I have suggested anyway. The second script is only loaded after the first one has completed. – Asad Saeeduddin Jul 31 '13 at 18:51
  • 1
    @Hamidam requirejs will load things asynchonously, however the loaded code has to be AMD compatible, so that requirejs can manage dependencies – darmce Jul 31 '13 at 18:53
  • @darmce If they are loading it async I want to know how? It has nothing to do with the AMD pattern... – mjs Jul 31 '13 at 19:09
  • @MoJS yeah i see what u mean, i was off.. i think you're out of luck, i think for a strict dependency require does it one after the other – darmce Jul 31 '13 at 20:29
  • @MoJS i just tested requirejs and if you set a shim dependency, it will not start (down)loading the script until the first one is done. Using the define()/require() structure you can avoid this though as Bergi mentions in his above post – darmce Jul 31 '13 at 20:41
  • @darmce Yeah, but Bergi also mentions the difficulty of understanding how they figure out what script is actually calling the define method. ( What if both are calling define, how does define know who called it? All they do is invoke define. Is it the first or the second one ). That is the bottom line question here I am trying to solve. I think I just figured it though, at least mentally. I will get back with the answer hopefully tomorrow, my head needs to rest I believe now :) – mjs Jul 31 '13 at 20:51