Background
I have written a shim to make EventTarget.addEventListener agnostic to scripts that have the async tag set. The problem with this kind of scripts is that you absolutely cannot tell when loading it has completed so even though you may register handlers for the DOMContentLoaded and/or load events, they aren’t guaranteed to fire.
This inevitably means that every asynchronous script needs to bring a logic that allows it to determine at which stage during the load process it has started to execute, however, this inevitably means some bloat to these scripts (since each of them has to bring this logic along, that’s extraneous code that could be avoided).
This shim installs a wrapper function to the aforementioned addEventListener and also registers its own handlers for both DOMContentLoaded and load (the former of which is also used to collect a list of all scripts present when the DOM is completed). So if a script now registers handlers for either of the two events, it still receives the events even though they may already have fired.
Installing it allows developers to write asynchronous JavaScript like one that is executed synchronously or with the defer tag set and still receive these two events if desired and so avoids extraneous code in these scripts. It also makes it easy to switch between synchronous and asynchronous.
Inner Workings
To achieve this the shim checks whether any of the events has already fired. If not, the handler is enqueued normally via the native function so that the browser can handle dispatching the events where applicable. However, if the event in question has already fired, an artificial event is generated and the handler added directly to the microtask queue, which essentially causes its immediate invocation.
This is the code of the shim:
'use strict';
(function (window, document, undefined) {
const orig_add_ev_listener = Object.getOwnPropertyDescriptor(EventTarget.prototype, 'addEventListener').value;
var dom_avail = false;
var page_loaded = false;
var scriptlist = [ ]; // Scripts defined prior to the DOMContentLoaded event
// This makes addEventListener aware to the firing of both the DOMContentLoaded
// and the load event.
// If an event handler is supposed to be registered on any of these events,
// add them as microtasks in case the events have already been dispatched.
function exec_event(p_event, p_handler, p_param)
{
var l_install_hand = true;
var l_custom_ev;
if(typeof p_event != 'string')
throw new TypeError('string expected');
if(typeof p_handler != 'function')
throw new TypeError('function expected');
if(typeof p_param == 'undefined')
p_param = { capture: false };
if(typeof p_param == 'boolean')
p_param = { capture: p_param };
if(typeof p_param != 'object' || Array.isArray(p_param))
throw new TypeError('boolean or non-array object expected');
switch(p_event)
{
case 'DOMContentLoaded':
if(this == document && dom_avail)
{
l_custom_ev = new Event('DOMContentLoaded');
l_install_hand = false;
}
break;
case 'load':
if(this == window && page_loaded)
{
l_custom_ev = new Event('load');
l_install_hand = false;
}
break;
}
if(l_install_hand)
orig_add_ev_listener.call(this, p_event, p_handler, p_param);
else
queueMicrotask(p_handler.bind(this, l_custom_ev));
}
document.addEventListener('DOMContentLoaded', p_event => {
var l_this;
dom_avail = true;
for(l_this of document.scripts)
scriptlist.push(l_this);
}, { once: true });
window.addEventListener('load', p_event => { page_loaded = true; }, { once: true });
Object.defineProperty(EventTarget.prototype, 'addEventListener', { value: exec_event });
})(window, document);
For synchronous or deferred scripts this is easy as they are going to get both events.
Asynchronous scripts, however, are treated a little differently, and depending on when the script is executed for the first time, the behavior of the wrapper differs:
- The document is still loading.
The handlers are registered normally with the browser, and once they are due, they are invoked normally.
DOMContentLoaded has already fired.
If a handler is registered for this event, instead of registering it with the browser, an artificial event is created and the handler directly added to the microtask queue. This has the handler being invoked right after the current job completes. A handler for load is still registered normally.
load has already fired.
An artificial event is created in either case and the respective handler added to the microtask queue.
The Problem
Normally a script that registers handlers for these events after they have fired doesn’t get them invoked any more so this shim is working around that. However, in its current form it cannot distinguish between scripts that have been defined while the document was still loading and those that have been injected later on so the latter still would have any handlers for DOMContentLoaded or load executed. To emulate the usual behavior for these latecomers I need to find out which script element they are residing in.
Is there a means available for native JavaScript to figure out the parent element of a particular function, something that I could vet against scriptlist?
Please note that the wrapper for addEventListener can be executed at arbitrary times so document.currentScript is not an option. Also, the function that needs to be checked is passed as an argument to the wrapper so it isn’t running so anything that relies on that isn’t available, either. Please also avoid constructs like caller, callee, etc. as they aren’t an option in strict mode.