Using getScript synchronously
Asked Answered
L

8

24

I'm writing an engine that requires the use of getScript quite extensively. I've pushed it into its own function, for ease of use, but now I need to make sure that the function itself is synchronous. Unfortunately, I can't seem to make getScript wait until the script it loads is actually finished loading before proceeding. I've even tried setting jQuery's ajax asynch property to false before making the call. I'm thinking of using jQuery's when/done protocol, but I can't seem to wrap my head around the logic of placing it inside a function and making the function itself synchronous. Any help would be very much appreciated!

function loadScript(script){
//Unrelated stuff here!!!
$.when(
$.getScript(script,function(){
    //Unrelated stuff here!!!
})).done(function(){
    //Wait until done, then finish function
});
}

Loop code (by request):

for (var i in divlist){
        switch($("#"+divlist[i]).css({"background-color"})){
            case #FFF:
            loadScript(scriptlist[0],divlist[i]);
            break;
        case #000:
            loadScript(scriptlist[2],divlist[i]);
            break;
        case #333:
            loadScript(scriptlist[3],divlist[i]);
            break;
        case #777:
            loadScript(scriptlist[4],divlist[i]);
            break;
    }
}
Liberty answered 8/2, 2013 at 23:29 Comment(5)
There is no need to make loadScript synchronous. Just return the promise object form $.getScript and let the calling code bind a callback. Or why exactly do you think it has to be synchronous?Stainless
I'm looping through a variable length array (varies by the page it's on) of divs using a for-in loop and performing a series of instructions with them, including loading scripts. The problem is, the scripts have to be loaded after the previous has finished loading and executing. Unfortunately, I've not been able to find a suitable way of "waiting" for an object to exist in javascript, yet. Intervals work well, except for the fact that it is all inside of that for-in loop for the div's.Liberty
Are you applying the same commands on each div (I assume so since it's a loop) but load a different script for each div? Could you post some code? It's easy to chain Ajax calls using deferred objects.Stainless
Updated with the basic idea. Perhaps you can think of up a better way of doing it.Liberty
I provided a suggestion.Stainless
S
11

As I said, it's relatively easy to chain Ajax calls with promise objects. Now, it don't see why the scripts have to be loaded one after the other, but you will have a reason for it.

First though I would get rid of the switch statement if you are only calling the same function with different arguments. E.g. you can put all the script URLs in a map:

var scripts = {
    '#FFF': '...',
    '#000': '...'
    // etc.
};

You can chain promises by simply returning another promise from a callback passed to .then [docs]. All you need to do is start with a promise or deferred object:

var deferred = new $.Deferred();
var promise = deferred.promise();

for (var i in divlist) {
    // we need an immediately invoked function expression to capture
    // the current value of the iteration 
    (function($element) {
        // chaining the promises, 
        // by assigning the new promise to the variable
        // and returning a promise from the callback
        promise = promise.then(function() {
            return loadScript(
                scripts[$element.css("background-color")], 
                $element
            );
        });
    }($('#' + divlist[i])));
}

promise.done(function() {
    // optional: Do something after all scripts have been loaded
});

// Resolve the deferred object and trigger the callbacks
deferred.resolve();

In loadScript, you simply return the promise returned from $.getScript or the one returned by .done:

function loadScript(script_url, $element){
    // Unrelated stuff here!!!

    return $.getScript(script_url).done(function(){
        //  Unrelated stuff here
        // do something with $element after the script loaded.
    });
}

The scripts will all be called in the order the are access in the loop. Note that if divlist is an array, you really should use normal for loop instead of a for...in loop.

Stainless answered 9/2, 2013 at 2:34 Comment(4)
deferred.resolve() throws an error (function doesn't exist) and the code doesn't execute as expected.Liberty
Mmmh. The deferred / promise objects changed a bit in the recent jQuery versions. I updated the example and hope it works now.Stainless
That did the trick. Thanks for not only solving my problem, but exposing me to a new method of performing chained tasks. This deferred/promise logic is very intriguing!Liberty
Yeah, it's great way to decouple code, abstract from synchronous/asynchronous code, etc. I love it :) You can read more about the original proposal for JavaScript (which was more basic) here: wiki.commonjs.org/wiki/Promises/A and of course on Wikipedia: en.wikipedia.org/wiki/Futures_and_promises.Stainless
R
34

This worked for me, and may help you.

$.ajax({
    async: false,
    url: "jui/js/jquery-ui-1.8.20.min.js",
    dataType: "script"
});

Basically, I just bypassed the shorthand notation and added in the async: false

Raychel answered 26/7, 2013 at 16:22 Comment(2)
That's the only method I found to do a sync "fallback" of some js files, without using "document.write". All others methods using DOM manipulation were always async even when setting async=false. The dom "appendBefore" occurs too late. The "xhr" method is really sync and on the next line, my variables are available to use. I know it's deprecated, but in how many years... document.write is also deprecated, and still works. And I think there's no other "sync" solution of scripts loading fallback directly in "html source".Cressler
Much appreciated for answering the question directly. I'm swimming in some legacy code at the moment, where handling things 'properly' isn't an option without massive refactors, so this is a good stop-gap for me.Kearns
S
11

As I said, it's relatively easy to chain Ajax calls with promise objects. Now, it don't see why the scripts have to be loaded one after the other, but you will have a reason for it.

First though I would get rid of the switch statement if you are only calling the same function with different arguments. E.g. you can put all the script URLs in a map:

var scripts = {
    '#FFF': '...',
    '#000': '...'
    // etc.
};

You can chain promises by simply returning another promise from a callback passed to .then [docs]. All you need to do is start with a promise or deferred object:

var deferred = new $.Deferred();
var promise = deferred.promise();

for (var i in divlist) {
    // we need an immediately invoked function expression to capture
    // the current value of the iteration 
    (function($element) {
        // chaining the promises, 
        // by assigning the new promise to the variable
        // and returning a promise from the callback
        promise = promise.then(function() {
            return loadScript(
                scripts[$element.css("background-color")], 
                $element
            );
        });
    }($('#' + divlist[i])));
}

promise.done(function() {
    // optional: Do something after all scripts have been loaded
});

// Resolve the deferred object and trigger the callbacks
deferred.resolve();

In loadScript, you simply return the promise returned from $.getScript or the one returned by .done:

function loadScript(script_url, $element){
    // Unrelated stuff here!!!

    return $.getScript(script_url).done(function(){
        //  Unrelated stuff here
        // do something with $element after the script loaded.
    });
}

The scripts will all be called in the order the are access in the loop. Note that if divlist is an array, you really should use normal for loop instead of a for...in loop.

Stainless answered 9/2, 2013 at 2:34 Comment(4)
deferred.resolve() throws an error (function doesn't exist) and the code doesn't execute as expected.Liberty
Mmmh. The deferred / promise objects changed a bit in the recent jQuery versions. I updated the example and hope it works now.Stainless
That did the trick. Thanks for not only solving my problem, but exposing me to a new method of performing chained tasks. This deferred/promise logic is very intriguing!Liberty
Yeah, it's great way to decouple code, abstract from synchronous/asynchronous code, etc. I love it :) You can read more about the original proposal for JavaScript (which was more basic) here: wiki.commonjs.org/wiki/Promises/A and of course on Wikipedia: en.wikipedia.org/wiki/Futures_and_promises.Stainless
S
7

Do you know that $.getScript accepts a callback function that is called synchronously after the script is loaded?

Example:

$.getScript(url,function(){
//do after loading script
});

I have 2 more solutions: a pure js one and one for multiple js load.

Stu answered 27/10, 2014 at 13:13 Comment(0)
W
4

Try this way, create array with deferred objects and used $.when with "apply"

var scripts = [
    'src/script1.js',
    'src/script2.js'
];

var queue = scripts.map(function(script) {
    return $.getScript(script);
});

$.when.apply(null, queue).done(function() {
    // Wait until done, then finish function
});
Willard answered 8/2, 2013 at 23:46 Comment(3)
My main problem is that I need it within a function (because I use the same 20 lines or so of code all over the place), and I need the function itself to be synchronous. That code just gives me a callback for when the scripts finish loading. getScript already has that feature.Liberty
Maybe then you should look at AMD architecture and use requirejs with modules dependencies? requirejs.org/docs/api.html#modulenameWillard
A decade later and I found this answer helpful! This answer is pretty under-rated. Yes getScript already has a callback, but this is nice if you're loading multiple scripts! Even using the straight ajax with async false was not working for me. I'm not sure why but I think even though the request to get the script was finished, it wasn't loaded and therefore my script would fail (only in Chrome) due to undefined names. This answer works perfectly for me.Gogol
R
0
var getScript = function(url) {
    var s = document.createElement('script');
    s.async = true;
    s.src = url;
    var to = document.getElementsByTagName('script')[0];
    to.parentNode.insertBefore(s, to);
};
Ratliff answered 9/2, 2013 at 0:1 Comment(0)
A
0

@Felix Kling's answer was a great start. However, I discovered that there was a slight issue with the overall attached .done() at the end of the .getScripts() returned result if I wanted to "functionalize" it. You need the last promise from the chained .getScript() iterations from within the loop. Here's the modified version of his solution (thank you, BTW).

Plugin:

(function ($) {
    var fetched = new function () {
            this.scripts = [];
            this.set = [];

            this.exists = function (url) {
                var exists = false;

                $.each(this.set, function (index, value) {
                    if ((url || '') === value) {
                        exists = true;

                        return false;
                    }
                });

                return exists;
            };

            this.buildScriptList = function () {
                var that = this;

                that.set = [];

                $('script').each(function () {
                    var src = $(this).attr('src') || false;

                    if (src) {
                        that.set.push(src);
                    }
                });

                $.merge(this.set, this.scripts);

                return this;
            };
        },
        getScript = $.getScript;

    $.getScript = function () {
        var url = arguments[0] || '';

        if (fetched.buildScriptList().exists(url)) {
            return $.Deferred().resolve();
        }

        return getScript
            .apply($, arguments)
            .done(function () {
                fetched.scripts.push(url);
            });
    };

    $.extend({
        getScripts: function (urls, cache) {
            if (typeof urls === 'undefined') {
                throw new Error('Invalid URL(s) given.');
            }

            var deferred = $.Deferred(),
                promise = deferred.promise(),
                last = $.Deferred().resolve();

            if (!$.isArray(urls)) {
                urls = [urls];
            }

            $.each(urls, function (index) {
                promise = promise.then(function () {
                    last = $.getScript(urls[index]);

                    return last;
                });
            });

            if (Boolean(cache || false) && !Boolean($.ajaxSetup().cache || false)) {
                $.ajaxSetup({cache: true});

                promise.done(function () {
                    $.ajaxSetup({cache: false});
                });
            }

            deferred.resolve();

            return last;
        }
    });
})($);

You can ignore the fetched function (I implemented it to reduce potential redundant calls - which is why I hijacked .getScript()) and see where the variable last is set inside the .getScripts() method. It defaults to a resolved deferred object, so that if the urls array is empty, it's passed to the returned result to attach the outer .done() call to. Otherwise, it will inevitably be assigned the last promise object from the chained .getScript() calls and thus will ensure everything will remain synchronous from outside the function.

Returning the initially created deferred object will not work if you resolve it before returning it back to the invoker (which is what you're supposed to do per jQuery's official documentation).

Example:

function loadStuff(data) {
    var version = {
        'accounting': '1.2.3',
        'vue': '1.2.3',
        'vueChart': '1.2.3'
    };

    $.getScripts([
        'https://cdnjs.cloudflare.com/ajax/libs/accounting.js/' + version.accounting + '/accounting.min.js',
        'https://cdnjs.cloudflare.com/ajax/libs/vue/' + version.vue + '/vue.min.js',
        'https://cdnjs.cloudflare.com/ajax/libs/vue-chartjs/' + version.vueChart + '/vue-chartjs.min.js'
    ], true)
        .done(function () {
            // do stuff
        })
        .fail(function () {
            throw new Error('There was a problem loading dependencies.');
        });
}
Anchusin answered 19/10, 2018 at 12:59 Comment(0)
G
-1

Just create a script node, set its src property to the JS you want to load then append it to the head:

var myScript = document.createElement('script');
myScript.src = "thesource.js";
document.head.appendChild(myScript);
Generalization answered 8/2, 2013 at 23:31 Comment(3)
That runs synchronously and makes sure the script is finished loading prior to executing any other code?Liberty
Right, scripts are loaded synchronously unless you add the async attributeGeneralization
I could have sworn they were loaded synchronously but it seems it's not the case, I'm sorry for misleading you.Generalization
R
-1

this is what I do

function loadJsFile(filename) {
    $.ajaxSetup({
        cache: true
    });

    var dloadJs = new $.Deferred();
    $.when(dloadJs).done(function () {
        $.ajaxSetup({
            cache: false
        });
    });

    dloadJs.resolve(
         $.getScript(filename, function () { })
    );
}
Raki answered 30/3, 2016 at 13:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.