Javascript, execute scripts code in order inserted into dom tree - Stack Overflow

admin2025-04-16  0

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

  • Load and execute javascript code SYNCHRONOUSLY
  • Loading HTML and Script order execution
  • Load and execute javascript code SYNCHRONOUSLY

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 binations.

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 == "plete"){
                                    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(".0.7/angular.min.js",function() {
  alert(1);  
})

loadScript(".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.

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

  • Load and execute javascript code SYNCHRONOUSLY
  • Loading HTML and Script order execution
  • Load and execute javascript code SYNCHRONOUSLY

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 binations.

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 == "plete"){
                                    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./ajax/libs/angularjs/1.0.7/angular.min.js",function() {
  alert(1);  
})

loadScript("http://ajax.googleapis./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.

Share edited May 23, 2017 at 12:31 CommunityBot 11 silver badge asked Jul 31, 2013 at 18:37 mjsmjs 22.4k32 gold badges133 silver badges220 bronze badges 12
  • @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 Commented Jul 31, 2013 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? – user1726343 Commented Jul 31, 2013 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 Commented Jul 31, 2013 at 19:05
  • I've had another go at the solution. Does this demo do what you were looking for? – user1726343 Commented Jul 31, 2013 at 21:29
  • 1 I forgot to actually append the script elements to the head. Here is a demo that does this: jsfiddle/Cucbq – user1726343 Commented Jul 31, 2013 at 21:55
 |  Show 7 more ments

5 Answers 5

Reset to default 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.

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 == "plete"){
                                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./ajax/libs/angularjs/1.0.7/angular.js",function() {
        alert(1);
});

loadScriptNew("http://ajax.googleapis./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 :)

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 pletion 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./ajax/libs/angularjs/1.0.7/angular.min.js",
   "http://ajax.googleapis./ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",
   "http://nowhere.existing.real_script..ar/return404.js"
];

// load all   even in reverse order ... or randomly
for(var a=0; a<scriptsToLoad.length; a++) loadScript(a, scriptsToLoad[a]);

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./ajax/libs/angularjs/1.0.7/angular.min.js?t=" + Date.now(), 
    "http://ajax.googleapis./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.

cant you nest the loading using ur callbacks?

ie:

loadScript("http://ajax.googleapis./ajax/libs/angularjs/1.0.7/angular.min.js",function() {
    alert(1);
    loadScript("http://ajax.googleapis./ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",function() {
        alert(2);  
    })
})
转载请注明原文地址:http://conceptsofalgorithm.com/Algorithm/1744807919a268109.html

最新回复(0)