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?
CodePudding user response:
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.
CodePudding user response:
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>
