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