4

On click, this outputs 1 then 2:

const myElem = document.getElementById("myElem")
myElem.addEventListener('click', () => queueMicrotask(() => console.log(1)));
window.addEventListener('click', () => console.log(2));
<button id="myElem">Click me</button>

If queueMicrotask is replaced with setTimeout it outputs 2 then 1. This means bubbling happens in microtasks, but not macrotasks.

Is it a part of DOM spec or just a browser implementation detail?

Phil
  • 157,677
  • 23
  • 242
  • 245
Alexey Berezkin
  • 1,513
  • 1
  • 10
  • 18
  • 1
    Not sure how you came to that conclusion. Calling `setTimeout` in place of `queueMicrotask` isn't changing anything as far as the event is concerned. – Ouroborus Jan 16 '22 at 22:07
  • "*This means bubbling happens in microtasks*" - no, it means that there are microtask checkpoints in the bubbling. And yes, this is part of the DOM spec. @Kaido can probably tell us where exactly – Bergi Jan 17 '22 at 00:19
  • 1
    @Bergi well as often it's https://html.spec.whatwg.org/multipage/webappapis.html#calling-scripts:clean-up-after-running-script, which in this case is called from https://webidl.spec.whatwg.org/#call-a-user-objects-operation itself from https://dom.spec.whatwg.org/#concept-event-listener-invoke – Kaiido Jan 17 '22 at 00:51
  • @Kaiido I see an item with "microtasks checkpoints" but unfortunately I don't see anything about micro- or macro-tasking handlers. What do I miss? – Alexey Berezkin Jan 17 '22 at 05:02
  • Yep sorry I messed my first link (took it directly from my browser's history), the correct one is https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script (which my first link linked to if you do follow). In there the third step says "If the JavaScript execution context stack is now empty, perform a microtask checkpoint." This rule basically makes it that every time a JS job is over, if the JS callstack is empty a microtask checkpoint will be performed. So here since the event are dispatch from a task the callstack will be empty between every callbacks. – Kaiido Jan 17 '22 at 05:07
  • If they were dispatched from a JS job (e.g by using `EventTarget.dispatchEvent`), the JS callstack wouldn't be empty (since the code calling dispatchEvent would still be "running"), and no microtask checkpoint would be performed in between. – Kaiido Jan 17 '22 at 05:09
  • @Kaiido thanks I see how microtasks are inserted between handlers. But how handlers of the same event are executed? Are they macro-tasked, i.e. inserted to event loop as separate tasks? – Alexey Berezkin Jan 17 '22 at 05:14
  • These are callbacks. The non-JS "task" is responsible of [dispatching the event](https://dom.spec.whatwg.org/#dispatching-events). If it's a proper task or not will depend on the type of event, for most events (e.g UI events) a task is queued in its task-source. For some events, (e.g resize, scroll, etc.) the callbacks are called directly from the "update the rendering" step of the event-loop, for some rare cases (e.g slotchange) it can even be dispatched from a microtask. For click that would be https://html.spec.whatwg.org/multipage/webappapis.html#user-interaction-task-source – Kaiido Jan 17 '22 at 05:31
  • @Kaiido so what really dispatches an event is non-JS "task" which has its own non-JS "queue", and upon user event a browser appends a "task" to that "queue". Then, between "callbacks", task dispatcher manually checks microtasks (overriding standard JS microtasking mechanism). Is that correct? – Alexey Berezkin Jan 17 '22 at 07:38
  • 1
    The browser does. Browsers are not written in JS. The JS microtasking is indeed disregarded in a browser, they do follow the [HTML event-loop processing model](https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model:event-loop), but luckily ES wrote it while looking at what HTML was doing at that time, so it's actually compatible. So yes, the OS sends a message to the browser that the device said something, the browser queues a task, later it will pick this task which will dispatch the event, and doing so execute the JS callbacks + microtask checkpoints in between. – Kaiido Jan 17 '22 at 07:46
  • @Kaiido thanks, it's clear now. Would you like to write an answer so I accept that? – Alexey Berezkin Jan 19 '22 at 05:47

2 Answers2

3

The only ways event bubbling can arise in a micro-queued task would be you dispatched a custom event on an element from within code being run from the microtask task queue, or if mutation observers are being internally signaled that the DOM has been modified (using "slotchange" events) from within a microtask. In both cases events are fired from within a microtask being executed, and event bubbling occurs synchronously in the JavaScript execution thread. In contrast, "native" events are fired by the browser and invoke event handlers asynchronously via the event loop.

Missing from the event scenario posted is that if multiple event handlers have been added for the same type of native event, the handlers are called from the event loop. Hence any microtasks enqueued by a handler will be executed before the control from the handler call returns to the event queue.

By way of demonstration of event flow for two "click" handlers:

"use strict";
document.querySelector("div").addEventListener("click", click1);
window.addEventListener("click", click2 /*, {capture:true}*/ );

function click1(event) {
   console.log("click1 called for div")
   queueMicrotask( ()=> console.log("microtask1 executes"));
   console.log("click1 exits");
}

function click2(event) {
   console.log("click2 called for window")
   queueMicrotask( ()=> console.log("microtask2 executes"));
   console.log("click2 exits");
}
div {background-color: yellow}
<div>Click this div</div>
or click the window,

Run the snippet and click the div to confirm that the div handler gets called, exits, and the microtask it spawned runs, before the window handler gets called after bubbling occurs. Note that although called from the event loop, the order they are called in will depend on whether event capture is used (commented out in the snippet).

If you replace queueMicrotask with setTimeout in the first click handler of the post, the call back function is no longer executed in the microtask queue before returning to the event loop, allowing the event loop to call the second click handler (for an event generated before timer expiry) before the timer callback.

traktor
  • 17,588
  • 4
  • 32
  • 53
  • So do you mean handlers are run from macrotasks which all are enqueued immediately on user event, and the thing with setTimeout is just adding to the nonempty queue end? – Alexey Berezkin Jan 17 '22 at 04:58
  • To be extremely pedantic, it *is* possible to have native events being fired from a microtask. For instance, the [slotchange](https://dom.spec.whatwg.org/#signaling-slot-change) is one of these: https://jsfiddle.net/1oafbk5L/ – Kaiido Jan 17 '22 at 05:19
  • 1
    @AlexBezkin I assume handlers are run from native browser code and called with a clean stack using the event loop. A good picture of event flow is provided in the W3C Dom 3 event specification under [" Event dispatch and DOM event flow"](https://www.w3.org/TR/DOM-Level-3-Events/#event-flow). Details of browser code that performs event capture, bubbling, moving events up the propagation path while updating Event object properties and responding to method calls, and calls event handlers via the event loop is outside my expertise, but personally would not call such code a "macro task". – traktor Jan 17 '22 at 07:41
  • @Kaiido Thank you for the update and fiddle, which I value highly. Although I have used mutation observers, I have not made use of custom elements and shadow DOMs before, or looked at the signalling mechanisms used to eventuate callbacks to user code with DOM update data. I did notice that the DOM standard talks of "firing an event" and ends up calling handleres in a `forEach` loop. This, in conjunction with the order of messages on the console, suggests the events were dispatched _synchronously_ in the manner of firing custom events on `eventTarget` elements - do you differ at all? – traktor Jan 17 '22 at 08:16
  • Well, no, they're not really fired synchronously, they are as part of the mutation observer microtask, which is thus by definition not synchronous since it's a microtask. Otherwise, yes it's dispatched "synchronously" from the point of view of the caller of dispatch-event, as it is always. – Kaiido Jan 17 '22 at 08:30
  • @traktor Thanks. So the phrase "handlers are called via event loop", as it's written in mdn, is not accurate. Actually js event loop with micro and macrotasks is not used, but dispatching all events is similar to fancy microtask which may also run other microtasks (checkpoints) from it. Funny that we are all told to study js event loop but in browser it works the different way which is not even mentioned in mdn – Alexey Berezkin Jan 19 '22 at 07:00
  • @AlexeyBerezkin Not so fast :-) The Mutation Observer API can dispatch events from within the microtask queue but like user events dispatched on an element, the event dispatch is synchronous: all eligible event handlers will be called in turn and control will return to the event dispatch caller _without going back to the event loop first._ This is not true for native events: if an individual event handler for "click" (say) enqueues a microtask, it will be executed before control returns to the event loop and before other handlers for the same event are called. MDN is correct but concise. – traktor Jan 19 '22 at 09:42
  • 1
    @AlexeyBerezkin watch out for over-simplications posted on the web. E.G. https://javascript.info/event-loop states "Tasks from the queue are processed on “first come – first served” basis." without mentioning that event loop has multiple task sources and task queues and can prioritize jobs based on the queue they come from [see also](https://stackoverflow.com/q/53627773/5217142) . This mechanism could come into play when handling UI events in browsers - or it could be that UI event handling simply executes the microtask queue between handler calls. The HTML standard doesn't cover the process. – traktor Jan 19 '22 at 10:15
  • @traktor Are there any good tutorials here on web that describe the real picture of tasks source and their priorities? I feel this part is totally ignored and described only in specifications – Alexey Berezkin Jan 19 '22 at 10:35
0

The difference in order isn't about event bubbling. It happens because the setTimeout -- like the click listeners themselves -- makes the execution of its anonymous function wait (at least) one full iteration of the event loop. The benefit of queueMicrotask is to avoid this extra delay.

This might help clarify the process:

const
  betweenTasks = (msg) => queueMicrotask( ()=>console.log(msg) ),
  futureTask = (msg) => setTimeout( ()=>console.log(msg), 0),      
  myElem = document.getElementById("my-elem");

myElem.addEventListener("click", () => {
  futureTask("callbacks to `setTimeout` must wait additional iteration(s)");
  betweenTasks("microtasks run between tasks");
  console.log("callbacks to event listeners must wait in the task queue");
});

window.addEventListener("click", () => {
  console.log("bubbled callbacks happen later than direct-target callbacks");
});
<button id="my-elem">Click me</button>
Cat
  • 4,141
  • 2
  • 10
  • 18