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;
    }
}
VashGH
  • 263
  • 1
  • 2
  • 8
  • 1
    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? – Felix Kling Feb 09 '13 at 00:51
  • 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. – VashGH Feb 09 '13 at 01:44
  • 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. – Felix Kling Feb 09 '13 at 01:46
  • Updated with the basic idea. Perhaps you can think of up a better way of doing it. – VashGH Feb 09 '13 at 02:05
  • I provided a suggestion. – Felix Kling Feb 09 '13 at 02:41

8 Answers8

33

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

jwheron
  • 2,553
  • 2
  • 30
  • 40
Sachseb1
  • 339
  • 1
  • 3
  • 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". – foxontherock Jul 10 '19 at 19:47
  • 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. – kmuenkel Mar 12 '21 at 09:02
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.

Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
  • deferred.resolve() throws an error (function doesn't exist) and the code doesn't execute as expected. – VashGH Feb 09 '13 at 03:22
  • Mmmh. The deferred / promise objects changed a bit in the recent jQuery versions. I updated the example and hope it works now. – Felix Kling Feb 09 '13 at 03:24
  • 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! – VashGH Feb 09 '13 at 03:30
  • 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: http://wiki.commonjs.org/wiki/Promises/A and of course on Wikipedia: https://en.wikipedia.org/wiki/Futures_and_promises. – Felix Kling Feb 09 '13 at 03:33
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.

Community
  • 1
  • 1
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
});
kupriyanenko
  • 374
  • 3
  • 4
  • 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. – VashGH Feb 08 '13 at 23:53
  • 1
    Maybe then you should look at AMD architecture and use requirejs with modules dependencies? http://requirejs.org/docs/api.html#modulename – kupriyanenko Feb 09 '13 at 00:04
  • 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. – ReenigneArcher Apr 04 '23 at 22:13
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);
};
yckart
  • 32,460
  • 9
  • 122
  • 129
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.');
        });
}
Erutan409
  • 730
  • 10
  • 21
-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);
Roger C
  • 338
  • 1
  • 5
  • That runs synchronously and makes sure the script is finished loading prior to executing any other code? – VashGH Feb 08 '13 at 23:39
  • Right, scripts are loaded synchronously unless you add the async attribute – Roger C Feb 08 '13 at 23:39
  • 2
    I could have sworn they were loaded synchronously but it seems it's not the case, I'm sorry for misleading you. – Roger C Feb 08 '13 at 23:43
-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 () { })
    );
}
Stuart Lawrence
  • 19
  • 1
  • 1
  • 6