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; } } 
5
  • 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? Commented Feb 9, 2013 at 0: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. Commented Feb 9, 2013 at 1: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. Commented Feb 9, 2013 at 1:46
  • Updated with the basic idea. Perhaps you can think of up a better way of doing it. Commented Feb 9, 2013 at 2:05
  • I provided a suggestion. Commented Feb 9, 2013 at 2:41

8 Answers 8

35

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

Sign up to request clarification or add additional context in comments.

2 Comments

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

4 Comments

deferred.resolve() throws an error (function doesn't exist) and the code doesn't execute as expected.
Mmmh. The deferred / promise objects changed a bit in the recent jQuery versions. I updated the example and hope it works now.
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!
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.
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.

Comments

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 }); 

3 Comments

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.
Maybe then you should look at AMD architecture and use requirejs with modules dependencies? requirejs.org/docs/api.html#modulename
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.
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); }; 

Comments

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.'); }); } 

Comments

-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); 

3 Comments

That runs synchronously and makes sure the script is finished loading prior to executing any other code?
Right, scripts are loaded synchronously unless you add the async attribute
I could have sworn they were loaded synchronously but it seems it's not the case, I'm sorry for misleading you.
-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 () { }) ); } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.