46

I've got a script with a DOMContentLoaded event handler—

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

Which I'm loading asynchronously—

<script async src=script.js></script>

However, the event handler is never called. If I load it synchronously—

<script src=script.js></script>

It works fine.

(Even if I change the DOMContentLoaded event to a load event, it's never called.)

What gives? The event handler should be registered irrespective of how the script is loaded by the browser, no?

Edit: It doesn't work on Chrome 18.0.1025.11 beta but, with DOMContentLoaded, it does on Firefox 11 beta (but with load it doesn't). Go figure.

OH GREAT LORDS OF JAVASCRIPT AND THE DOM, PRAY SHOW THE ERROR OF MY WAYS!

user1203233
  • 463
  • 1
  • 4
  • 5
  • 1
    By loading the script asynchrounously, you are telling the browser that it can load that script independently of the other parts of the page. That means that the page may finish loading and may fire DOMContentLoaded BEFORE your script is loaded and before it registers for the event. If that happens, you will miss the event (it's already happened when you register for it). – jfriend00 Feb 11 '12 at 01:44
  • Oh—interedasting. So really I should be testing for the event having fired already—(**edit**: actually, since I rely on the event firing *after* the script, it shouldn't be asynchronous at all). Cool. Write it as an answer and you can have an upvote :) – user1203233 Feb 11 '12 at 01:47

4 Answers4

79

By loading the script asynchronously, you are telling the browser that it can load that script independently of the other parts of the page. That means that the page may finish loading and may fire DOMContentLoaded BEFORE your script is loaded and before it registers for the event. If that happens, you will miss the event (it's already happened when you register for it).

In all modern browsers, you can test the document to see if it's already loaded (MDN doc), you can check:

if (document.readyState !== "loading")

to see if the document is already loaded. If it is, just do your business. If it's not, then install your event listener.

In fact, as a reference source and implementation idea, jQuery does this very same thing with it's .ready() method and it looks widely supported. jQuery has this code when .ready() is called that first checks to see if the document is already loaded. If so, it calls the ready function immediately rather than binding the event listener:

// Catch cases where $(document).ready() is called after the
// browser event has already occurred.
if ( document.readyState === "complete" ) {
    // Handle it asynchronously to allow scripts the opportunity to delay ready
    return setTimeout( jQuery.ready, 1 );
}
mh-anwar
  • 131
  • 3
  • 10
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Yes sir that's all I needed to know. I lied about the upvote—haven't got enough rep ;) I hope someone is kind enough to up-vote this for you though :) – user1203233 Feb 11 '12 at 01:55
  • In case `document.readyState` is Gecko-only: the way around that is running another `DOMContentLoaded` event handler *synchronously* which will set a global flag when run, which we can check against afterwards :) **Edit**: Oh wait all the cool browsers support it so we fine – user1203233 Feb 11 '12 at 01:59
  • 7
    Trying things out, it turns out checking for `document.readyState == 'complete'` is *not sufficient* — when the check was occurring, `document.readyState` was `interactive`. Checking for `document.readyState != 'loading'` works fine though :) – user1203233 Feb 11 '12 at 14:35
  • There is not a lot of documentation on the values for `document.readyState`, but this is what I could find: `One of the following values (as strings): complete | interactive | loading | uninitialized. Some elements may allow the user to interact with partial content, in which case the property may return interactive until all loading has completed.` So, it would depend upon which state you want. – jfriend00 Feb 11 '12 at 15:07
  • 4
    From what I read of [WHATWG's draft](http://www.whatwg.org/specs/web-apps/current-work/multipage/dom.html#dom-document-readystate) (scroll up a bit) — *Returns "loading" while the Document is loading, "interactive" once it is finished parsing but still loading sub-resources, and "complete" once it has loaded. (Non-normative)*. In the end I made a check for `document.readyState == 'loading'` and registered the event handler in that – user1203233 Feb 11 '12 at 15:48
  • 2
    @user1203233 - I know this is old, but I'll add this comment for the benefit of others. There are comments and bugs in the jQuery development process related to using `document.readyState == 'loading'` in some versions of IE. It simply doesn't work (it fires too early). As such jQuery only uses `document.readyState === 'complete'`. These bugs are not easy to reproduce because they only occur in certain situations with certain server send buffer sizes and certain size documents. But, it can be reproduced and is a real bug in IE. – jfriend00 May 25 '14 at 15:25
  • You'd probably want to use `if (document.readyState !== "loading")` because it might be in `"interactive"` state your code will never run. – Steven Vachon Jan 08 '16 at 00:55
  • @StevenVachon - I think I agree. I updated my answer accordingly. Unfortunately, there are a number of browser bugs in certain browser versions around when the state is is switched out of `"loading"` that complicate this. – jfriend00 Jan 08 '16 at 01:33
  • So to be clear if it's a small and cheap operation I can just put it `do_it(); document.addEventListener('DOMContentLoaded', function() { do_it(); });` And it may fire twice but always at least once? Because I want to make sure it gets applied to all elements and when I load it in `` with `asyc` not all elements are ready yet, so I want to make sure its fired when all elements are loaded. – redanimalwar Jan 25 '21 at 20:33
  • @redanimalwar - Yes, it will fire once and only once as long as you apply the event listener before the event has fired. So, adding the event listener in the `` tag or at the beginning of the `` tag (not in an `async` script) would be good. – jfriend00 Jan 25 '21 at 23:21
30

This is not the final answer but made me understand why is not correct using async with a script that need to modify DOM, so must wait to DOMContentLoaded event. Hope could be beneficial.

enter image description here

(Source: Running Your Code at the Right Time from kirupa.com)

Community
  • 1
  • 1
manus
  • 499
  • 10
  • 13
13

Most vanilla JS Ready functions do NOT consider the scenario where the DOMContentLoaded handler is initiated after the document already has loaded - Which means the function will never run. This can happen if you use DOMContentLoaded within an async external script (<script async src="file.js"></script>).

The code below checks for DOMContentLoaded only if the document's readyState isn't already interactive or complete.

var DOMReady = function(callback) {
  document.readyState === "interactive" || document.readyState === "complete" ? callback() : document.addEventListener("DOMContentLoaded", callback);
};
DOMReady(function() {
  //DOM ready!
});

If you want to support IE aswell:

var DOMReady = function(callback) {
    if (document.readyState === "interactive" || document.readyState === "complete") {
        callback();
    } else if (document.addEventListener) {
        document.addEventListener("DOMContentLoaded", callback);
    } else if (document.attachEvent) {
        document.attachEvent("onreadystatechange", function() {
            if (document.readyState != "loading") {
                callback();
            }
        });
    }
};

DOMReady(function() {
  // DOM ready!
});
Alexandrin Rus
  • 4,470
  • 2
  • 16
  • 29
Null
  • 975
  • 13
  • 13
  • You could also use `document.readyState !== "loading"` instead of the OR ([values](https://developer.mozilla.org/docs/Web/API/Document/readyState#values)) – SWdV Aug 14 '21 at 16:58
4

One way around this is to use the load event on the window object.

This will happen later than DOMContentLoaded, but at least you don't have to worry about missing the event.

window.addEventListener("load", function () {
   console.log('window loaded');
});

If you really need to catch DOMContentLoaded event you can do use Promise object. Promise will get resolved even if it happened earlier:

HTMLDocument.prototype.ready = new Promise(function (resolve) {
if (document.readyState != "loading")
    return resolve();
else
    document.addEventListener("DOMContentLoaded", function () {
        return resolve();
    });
});

document.ready.then(function () {
    console.log("document ready");
});
user4617883
  • 1,277
  • 1
  • 11
  • 21