I'm confused regarding events not firing in a particular ES6 module loading scenario. Here is a Codepen project to support my examples:
Consider this very simple module app.js:
console.log('in module');
document.addEventListener('DOMContentLoaded', (event) => {
console.log('DCL:2');
});
I'm using dynamic ES6 imports from script tags in the head of the document:
<script>
(async () => {
const module = await import("./js/app.js");
console.log('async done');
})();
</script>
So, this calling code is a non-module (sync script) dynamically loading a module. Because it is in the head of the document and not async or defer, I expect it to run immediately, and to do so in a blocking way.
That's my expectation, yet not what is happening. In particular, the problem is that the event in the module is never fired. And it looks to be because it's too late to listen to it, the event has already happened. How can this be, given the blocking script in the head of the document? It looks to be non-blocking, yet I don't understand why.
To be clear, the discussion isn't whether this is a good idea or not, I'm above all interested in the conceptual answer of why this doesn't work. Or to rephrase the question: how can a non-module script dynamically load a module in a synchronous way?
I'm confused regarding events not firing in a particular ES6 module loading scenario. Here is a Codepen project to support my examples: https://codepen.io/fchristant/project/editor/AYQkGJ
Consider this very simple module app.js:
console.log('in module');
document.addEventListener('DOMContentLoaded', (event) => {
console.log('DCL:2');
});
I'm using dynamic ES6 imports from script tags in the head of the document:
<script>
(async () => {
const module = await import("./js/app.js");
console.log('async done');
})();
</script>
So, this calling code is a non-module (sync script) dynamically loading a module. Because it is in the head of the document and not async or defer, I expect it to run immediately, and to do so in a blocking way.
That's my expectation, yet not what is happening. In particular, the problem is that the event in the module is never fired. And it looks to be because it's too late to listen to it, the event has already happened. How can this be, given the blocking script in the head of the document? It looks to be non-blocking, yet I don't understand why.
To be clear, the discussion isn't whether this is a good idea or not, I'm above all interested in the conceptual answer of why this doesn't work. Or to rephrase the question: how can a non-module script dynamically load a module in a synchronous way?
Share Improve this question asked May 5, 2020 at 11:38 FerFer 4,19618 gold badges62 silver badges105 bronze badges 6-
2
If you want to load the script in a synchronous way, then you should just add it to the document in a
<script>
tag. The advantage of modules is that you can use different functions from other scripts in a single script by importing them and calling them there. In your case, your file is downloaded, but there is no exported function in yourapp.js
script, so there is nothing to call or to use. – Emiel Zuurbier Commented May 5, 2020 at 11:46 - What should I export then? The code inside the module clearly is running, as "in module" is printed. Yet by the time it runs, domcontentloaded has already happened. I still don't have an answer as to why this is. – Fer Commented May 5, 2020 at 11:52
-
1
Importing your module asynchronously tells the browser that it should not wait for the module before emitting
DOMContentLoaded
event. This is almost the same as adding theasync
attribute to aSCRIPT
tag. If you want your module to handle theDOMContentLoaded
event - you should not use async importing. – IVO GELOV Commented May 5, 2020 at 11:57 -
async
functions are non-blocking - so there is no "blocking script in the head" – Jaromanda X Commented May 5, 2020 at 11:58 -
You could put the
domcontentloaded
event listener in your main script and import your module when the event has occurred. – Emiel Zuurbier Commented May 5, 2020 at 11:59
3 Answers
Reset to default 6Wait for the domcontentloaded
event to happen in your index.html
file. In the callback of the event import the module that you want to run whenever the event has fired.
// app.js
export function start() {
console.log('in module');
console.log('DCL:2');
}
<!-- index.html -->
<script>
document.addEventListener('DOMContentLoaded', async (event) => {
const module = await import("./js/app.js");
module.start();
console.log('async done');
});
</script>
Or if you don't need any modules and you simply want everything to load one after another before the DOM has been rendered, just use the traditional render-blocking <script>
tags.
<script src="./js/app.js"></script>
<script>
console.log('module loaded');
</script>
You could use a helper function to check document.readyState
and then either addEventListener
or run code immediately.
onDCL1()
can be used with a callback fn (A.1)onDCL2()
is Promise based- … traditional
.then()
chain (B.1) - … or
await
for it (C.1)
- … traditional
We can "fake" and dispatch our own DOMContentLoaded
event and verify that else
block works (A.2. , B.2. , C.2.)
class App {
static onDCL1(callback) {
if (document.readyState !== 'loading')
callback(`onDCL1 ➜ readyState: '${document.readyState}'`);
else
document.addEventListener('DOMContentLoaded',
e => callback(`onDCL1 ➜ ... waited readyState: '${document.readyState}'`));
}
static onDCL2() {
return new Promise(resolve => {
if (document.readyState !== 'loading')
resolve(`onDCL2 ➜ readyState: '${document.readyState}'`);
else
document.addEventListener('DOMContentLoaded',
e => resolve(`onDCL2 ➜ ... waited readyState: '${document.readyState}'`));
});
}
}
(async() => {
console.log(`readyState: '${document.readyState}'`);
// A.1
App.onDCL1(payload => console.log(`A.1.| ${payload}`)); // run callback
// B.1
App.onDCL2().then(payload => console.log(`B.1.| ${payload}`)); // Promise chain
// C.1
let payload = await App.onDCL2(); // await for Promise. Hint: no 'async' required, since we already return a Promise
console.log(`C.1.| ${payload}`);
// have to wait for C.1. (`await`) ....
// ... ment 2 lines of "C.1" will show 'END' bevor Promise B.1. gets resolved async (~1ms)
console.log('END 1');
setTimeout(async() => {
// dispatch "fake" event in 100ms, (we need to addEventListeners first
// ... since C.2 has `await` we do this here
setTimeout(() => {
window.document.dispatchEvent(new Event("DOMContentLoaded", {
bubbles: true,
cancelable: true
}));
}, 100);
// A.2
App.onDCL1(payload => console.log(`A.2.| ${payload}`)); // run callback
// B.2
App.onDCL2().then(payload => console.log(`B.2.| ${payload}`)); // Promise chain
// C.2
let payload = await App.onDCL2(); // await for Promise
console.log(`C.2.| ${payload}`);
console.log('END 2');
}, 300);
})();
You could also use an interval to run the code once the module has loaded:
const interval = setInterval(() => {
if(document.readyState!=="loading") {
clearInterval(interval);
... your code here ...
}
},250)