62

Is there a way to load and execute a javascript file in a synchronous way just like a synchronous XMLHttpRequest?

I'm currently using a sync XMLHttpRequest and then eval for this, but debugging that code is very difficult...

Thanks for your help!

Update

I tried this now:

test.html

<html>
    <head>
        <script type="text/javascript">
            var s = document.createElement("script");
            s.setAttribute("src","script.js");
            document.head.appendChild(s);
            console.log("done");
        </script>
    </head>
    <body>
    </body>
</html>

script.js

console.log("Hi");

Output: done Hi

So it was not executed synchronously. Any idea to make "Hi" appear first?

Update 2 Other example

test.html (code inside a script tag)

var s = document.createElement("script");
s.setAttribute("src","script.js");
document.head.appendChild(s);
SayHi();

script.js

function SayHi(){
    console.log("hi");
}

Output: Uncaught ReferenceError: SayHi is not defined

Van Coding
  • 24,244
  • 24
  • 88
  • 132
  • 4
    Not sure I understand - JavaScript *is* executed synchronously unless the `deferred` attribute is used in the `script` tag. Can you show an example of what you mean? – Pekka May 20 '11 at 16:21
  • 8
    Sorry, not true. When you load a JS script dynamically through code, unlike when it's included in the markup (like the asker did) - by default it's always loaded asynchroniously. See rest of thread below. – Gilad Barner Apr 19 '15 at 06:54
  • 4
    document.write() is the only way to synchronously insert script tag. – Kevin He Apr 04 '19 at 00:36
  • 2
    Does this answer your question? [Dynamically loading JavaScript synchronously](https://stackoverflow.com/questions/2879509/dynamically-loading-javascript-synchronously) – Hirurg103 Feb 29 '20 at 17:19

7 Answers7

33

If you use this:

function loadScriptSync (src) {
    var s = document.createElement('script');
    s.src = src;
    s.type = "text/javascript";
    s.async = false;                                 // <-- this is important
    document.getElementsByTagName('head')[0].appendChild(s);
}

You can do what you want (although divided up in an additional script file)

test.html (code inside a script tag):

loadScriptSync("script.js");
loadScriptSync("sayhi.js"); // you have to put the invocation into another script file

script.js:

function SayHi() {
     console.log("hi");
}

sayhi.js:

SayHi();
Adrian Günter
  • 903
  • 2
  • 11
  • 23
heinob
  • 19,127
  • 5
  • 41
  • 61
  • still get error by using this: loadScriptSync("https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js"); $().jquery; – Shawn Nov 15 '16 at 03:11
  • Not sure what you are saying about separate scripts, but nowhere in the loadScriptSync function is anything blocking until the script is loaded. – Michael May 02 '17 at 02:20
  • 1
    Does s.async = false; actually do anything. When debugging, that does not set it to async=false on the object – Drenai Aug 01 '17 at 17:02
  • 9
    `loadScriptSync` doesn’t execute it inline. With `s.async = false`, scipts are just loaded *in order*. so loading multiple scripts that way guarantees that they’re executed in order. – flying sheep Oct 20 '17 at 06:41
  • 4
    this is loading ASYC only.. Not synchronous – mythicalcoder Jan 21 '19 at 17:45
26

All scripts which are loaded after DOM is ready are loaded asynchronously. The only reason for browser to load them synchronously is function write which can output something. So you can use onload callback of the script element to achieve what you want.

var s = document.createElement("script");
s.setAttribute("src","script.js");
s.onload = function(){
    console.log('Done');
}
document.head.appendChild(s);

Another way is to load js-file via XHR and set code inside the script element:

window.onload = function(){
    var req = new XMLHttpRequest();
    req.open('GET', "test.js", false);
    req.onreadystatechange = function(){
        if (req.readyState == 4) {
            var s = document.createElement("script");
            s.appendChild(document.createTextNode(req.responseText));
            document.head.appendChild(s);
        }
    };
    req.send(null);
}
bjornd
  • 22,397
  • 4
  • 57
  • 73
  • 10
    this is not synchronous ;) – Van Coding May 20 '11 at 16:44
  • 2
    Yeah, but there is no another solution as far as I can judge. Moreover your question was: "Any idea to make "Hi" appear first?", not "How to do that synchronously?". – bjornd May 20 '11 at 16:45
  • 2
    Then I have to use eval? It was ment to be synchronous as it is in the title of the question. – Van Coding May 20 '11 at 16:47
  • What the reason for using eval? – bjornd May 20 '11 at 16:48
  • 1
    Then I can make it synchronous. I can use a synchronous XMLHttpRequest to load the code and then use eval to execute it. It seems to be the only way... – Van Coding May 20 '11 at 16:49
  • Indeed!!! This works! But then I have the same as eval()... I don't get the exact line numbers for errors... – Van Coding May 20 '11 at 16:55
  • @FlashFan: Maybe you should explain (in your question) the whole story behind it. Maybe you're trying to solve something in the completely wrong way. Maybe we can help by suggesting better ways of doing it. Synchronous XHRs will be more of a problem than solution and we wouldn't recommend them. – Robert Koritnik May 20 '11 at 16:59
14

From a similar question ( https://stackoverflow.com/a/3292763/235179 ):

<script type="text/javascript">
  document.write('<script type="text/javascript" src="other.js"><\/script>');
</script>

<script type="text/javascript">
  functionFromOther();
</script>

Either the code called from the document.write'd script needs to be in it's own <script> or it needs to be in the window.onload() event.

Community
  • 1
  • 1
Josh Johnson
  • 10,729
  • 12
  • 60
  • 83
5

Your scripts do execute synchronously

your code if put together is:

1. create script element
2. set its attribute src
3. set its attribute deferred
4. display done...

this first part stops execution and hands it over to next script

5. script executes and displays Hi

Everything is very much synchronous... In Javascript some code is executed completely until it executes to the last line or hands execution over to internal systems (like XHR or timer).

When one would like to put prepare some parts to execute later on, they prepare it with setTimeout. Even if timeout is shorter than the rest of the code will take that's the time it will execute. After code has finished executing. Example:

// some code
setTimeout(function(){ alert("I'm second alert"); }, 1);
longExecutionTask();
alert("I'm the first alert");

In the above code even if setTimeout is set to execute after 1ms it won't start until the code after it finishes execution which ends with displaying an alert box. The same happens in your case. The first batch of code has to finish executing before anything else can start.

Why you're getting exception (in example 2)

You've added some more code after I've written my answer so here goes some more info.

Adding a script tag will not immediately execute it. Script loading+execution will happen when HTML parser will get to the SCRIPT element you added. It will load it at that point and evaluate/execute its content.

  1. HTML parser starts parsing your document
  2. HEAD is being parsed and its SCRIPT child tag gets parsed and executed. This execution adds one more element to BODY tag that hasn't been parsed yet.
  3. Parser moves on to BODY and parses its content (the newly added SCRIPT tag) which then loads the script and executes its content.

SCRIPT elements get immediately executed only when you they're added after your page has been parsed and is already rendered in browser. In your case that is not the case. The first script executes immediately and the dynamically added one executes when parses gets to it.

Robert Koritnik
  • 103,639
  • 52
  • 277
  • 404
  • 1
    No... The console displays "done" first and later "Hi" so it was not executed when the script tag was added ;) That is my problem. I want to be able to access code inside the tag, for example a function, direclty after adding the tag- – Van Coding May 20 '11 at 16:37
  • @FlashFan: Check my additional information about javascript code execution process. It should make things clear for you. – Robert Koritnik May 20 '11 at 16:39
  • 1
    @Robert: But then it is not synchonous, and that is not the question. I'm asking for a way to execute the code immediately & without using eval. Is it possible? – Van Coding May 20 '11 at 16:42
  • @FlashFan: Synchronous execution in my dictionary means procedural/serial execution when you know exactly when something is going to execute. Asynchronous on the other hand depends on external factors when you cant say for sure when something will execute and in what order. So your code is very much sync. – Robert Koritnik May 20 '11 at 16:47
  • Ok, then I ask it different for you: Is it possible to load & execute script before continuing & without using eval? – Van Coding May 20 '11 at 16:51
  • FlashFan: Yes it is. Don't use immediate script execution. And use `load` events on your scripts that you wish to first execute (check @bjornd's example). jQuery DOM ready event will be helpful. But based o your understanding I suppose this will be a very hard nut to crack for you. You have to understand browsers and their execution cycle quite a bit. – Robert Koritnik May 20 '11 at 16:56
  • 2
    @RobertKoritnik, So according to you answer is NO. – LNT Jul 17 '15 at 15:59
2

You can synchronize asynchronous operations among themself. Create recursive function to represent a loop and call the next operation when previous finish. The following function imports scripts in order with the similar technique. It waits a script to be loaded and if there is no error continues with the next. If an error occur it calls the callback function with the error event and if there is no error - calls the same callback function with null after all scripts are loaded. It also cleans after itself with s.parentNode.removeChild(s).

function importScripts(scripts, callback) {
    if (scripts.length === 0) {
        if (callback !== undefined) {
            callback(null);
        }
        return;
    }
    var i = 0, s, r, e, l;
    e = function(event) {
        s.parentNode.removeChild(s);
        if (callback !== undefined) {
            callback(event);
        }
    };
    l = function() {
        s.parentNode.removeChild(s);
        i++;
        if (i < scripts.length) {
            r();
            return;
        }
        if (callback !== undefined) {
            callback(null);
        }
    };
    r = function() {
        s = document.createElement("script");
        s.src = scripts[i];
        s.onerror = e;
        s.onload = l;
        document.head.appendChild(s);
    };
    r();
}
User0123456789
  • 295
  • 3
  • 8
1

You can use the "defer" attribute when including the js file.

<script defer="defer" src="contact/js/frontend/form.js"></script>
user1077915
  • 828
  • 1
  • 12
  • 26
0

I've been using a javscript on-and-off, and I've been bumping into this question for years. As usual I spent some time analyzing, but this is the first time I've reached some sort of a resolution - so, I'll take the time to post a writeup (you can thank me later :)).

Let's say, in <head>, you have <script src="script_first.js"></script>, where you have the following code in the file script_first.js:

const js_to_load = [
  "https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.js",
  "https://unpkg.com/whatwg-fetch@2.0.3/fetch.js",
];

for(let js_url of js_to_load) {
  loadScriptSync(js_url);
}

... where loadScriptSync is from answer by @heinob. If you add a breakpoint inside the for, and have the JavaScript debugger stop there, then step once more, you will notice that at that point (after loadScriptSync has executed once):

  • The new <script> element has already been added to the DOM
  • The Network tab will show that the script src has already been loaded

This might motivate one to add a property to the newly added <script> tags, to manage whether the script has loaded. There seems to be no other way in JavaScript HTML DOM to detect this, other than to use the onload handler. So I tried something like this:

function pausecomp(millis) // https://stackoverflow.com/q/951021 -> http://www.sean.co.uk/a/webdesign/javascriptdelay.shtm
{
  var date = new Date();
  var curDate = null;
  do { curDate = new Date(); }
  while(curDate-date < millis);
}

function loadScriptSync (src) {
  //import src; // SyntaxError: import declarations may only appear at top level of a module
  var s = document.createElement('script');
  s.has_loaded = false;
  s.onloadeddata_var = false;
  s.type = "text/javascript";
  s.async = false;                                 // <-- this is important
  s.onload = function(e) { // "set the src attribute after the onload event" https://stackoverflow.com/q/16230886
    let myscript = e.target;
    myscript.has_loaded = true;
  }
  s.src = src;
  document.getElementsByTagName('head')[0].appendChild(s);
  while (! (s.has_loaded)) {
    //await new Promise(r => setTimeout(r, 200)); // sleep, https://stackoverflow.com/q/951021 // SyntaxError: await is only valid in async functions, async generators and modules
    pausecomp(200);
  };
}

This basically ends up as an endless loop, preventing the rest of the page to load. Conclusion:

  • The onload event of the newly added <script> will NOT fire - even if the Network tab show that the script src has loaded - for as long as we're still inside the context of the script that created it (here "script_first.js"`)

This means that:

  • We'll have to use a second .js script in the HTML page, to inspect whether the newly added scripts have loaded - and wait until they all do
  • In this second script, we'll have to use async ways to sleep while waiting for scripts to load, so we can allow the rest of the page to render "in the meantime" while sleeping - else, with synchronous sleep, we'll block page rendering again, and the onload events will not fire.

That being said, the script_first.js now becomes:

// const js_to_load - same as above
const js_load_scripts = [];

function loadScriptSync (src) {
  //import src; // SyntaxError: import declarations may only appear at top level of a module
  var s = document.createElement('script');
  s.has_loaded = false;
  s.onloadeddata_var = false;
  s.type = "text/javascript";
  s.async = false;                                 // <-- this is important
  s.onload = function(e) { // "set the src attribute after the onload event" https://stackoverflow.com/q/16230886
    let myscript = e.target;
    myscript.has_loaded = true;
  }
  s.src = src;
  js_load_scripts.push(s);
  document.getElementsByTagName('head')[0].appendChild(s);
}

for(let js_url of js_to_load) {
  loadScriptSync(js_url);
}

document.addEventListener('DOMContentLoaded', function() {
  console.log('DOMContentLoaded script_first');
}, false);

... and add <script src="script_second.js"></script> at the end of the .html document (I've tried both immediately before, and after, the ending </body> tag, and both locations worked), where script-second.js is:

//const js_load_scripts = []; // there is a global var here from script_first.js

async function wait_for_load_scripts() {
  let is_ready = false;
  while (!(is_ready)) {
    let temp_is_ready = true;
    await new Promise(r => setTimeout(r, 500)); // https://stackoverflow.com/q/951021
    for (let tscript_el of js_load_scripts) {
      temp_is_ready &= tscript_el.has_loaded;
    }
    is_ready = temp_is_ready;
  }
  return new Promise((resolve) => { resolve("loaded"); }); // return the string value "loaded"
}

let load_is_ready = await wait_for_load_scripts(); // `load_is_ready` variable should have the value "loaded" after function returns
console.log("after wait_for_load_scripts")

document.addEventListener('DOMContentLoaded', function() {
  console.log('DOMContentLoaded script_second');
}, false);

// at this point, all js_load_scripts have been loaded,
// and can proceed with javascript code that needs them below:
// ...

You will notice here that, since in script_second.js now we first wait for all scripts from script_first.js to load, "DOMContentLoaded script_first" will be printed in the log of the JavaScript Console before "after wait_for_load_scripts" - and consequently, "DOMContentLoaded script_second" is never printed (since we add the event handler after the event has already fired).

So, I guess, in response to the question:

Is it possible to load & execute script before continuing & without using eval?

... I'd answer, as a more concrete iteration of the "You can do what you want (although divided up in an additional script file)" in the answer by @heinob - as:

  • probably not in a single .js file (since, as the example above shows, in a single .js file in a browser you cannot know if "onload" has fired nor wait for it, if you've created the object in the same file)
  • yes if split in two .js files (although the example above is only certain to check whether all external have loaded, not necessarily if they also finished executing as well).
sdbbs
  • 4,270
  • 5
  • 32
  • 87