console.log("11111");
setTimeout(() => {
console.log("22222");
}, 5000);
Promise.resolve().then(() => {
console.log("33333");
});
document.getElementById("test").addEventListener("click", () => {
console.log("44444 +");
});
// 10 sec sync delay. This gives me enough time to stack them into appropriate Queues.
const syncWait = (ms) => {
const end = Date.now() + ms;
while (Date.now() < end) continue;
};
syncWait(10000);
console.log("55555");
Output:
11111
55555
33333
44444 +
22222
What is the exact priority of callback functions triggered using addEventListener
compared to microtask and macrotask queue callbacks in the Event Loop? In other words, during which exact phase of the Event Loop are they dequeued from their respective queues?
2
Answers
In the Event Loop, the priority of callback functions triggered using addEventListener compared to microtask and message queue callbacks depends on the specific phase of the Event Loop.
Let’s break down the execution order in your code:
Synchronous code: The initial synchronous code is executed first. So the output starts with:
Microtasks: After the synchronous code, the microtask queue is processed. In this case, the microtask is a resolved Promise callback. The resolved Promise callback is enqueued into the microtask queue using
So the output becomes:
Task queue (message queue): After processing the microtask queue, the Event Loop moves on to the task queue. Here, the only task in the task queue is the setTimeout callback function, which is going to execute after a delay of 5000 millisecs (5 sec). The setTimeout callback function is dequeued from the task queue, and its output is:
User interaction: At this point, the Event Loop is idle, waiting for user interaction. When the user clicks on the element with the id "test," the click event callback function (() => { console.log("44444 +"); }) is enqueued into the task queue. Since the user interaction occurred after the setTimeout callback function was dequeued, the output stays the same:
Microtasks: After processing the task queue, the Event Loop goes back to the microtask queue. However, in this case, there are no more microtasks in the queue, so the output remains the same:
Task queue (message queue): Finally the Event Loop goes back to the task queue. The only remianing task in the task queue is the click event callback function (() => { console.log("44444 +"); }). The callback function is dequeued and executed, resulting in the output:
Task queue (message queue): After processing the task que, the Event Loop goes back to the task queue one last time. The setTimout callback function, which was scheduled to execute after a delay of 5 seconds, is now ready to be dequeued. The output becomes:
So basically, the priority of callback functions triggered using addEventListner is
They are dequeued from the task queue during the task queue processing phase of the Event Loop. Hope that helps.
EventListener
callbacks are called synchronously. There is no priority for them. They may be called in any phase of the event loop.Wait, what?
Yes, it may seem like hair splitting, but what is queued are tasks, not the event callbacks. You can even trigger these callbacks yourself synchronously:
This does matter, because whether your JS callback has been passed to the API, or attached to the
EventListener
, has no incidence at all with regards to the priority system.To understand the task priority system, we have to look at how the event loop is defined in HTML.
And particularly, we’ll be interested in its second operation, which is to pick a task from one of the possibly various task-queues, and execute it. Note that the specs don’t really ask that there are multiple task queues, but they ask that there are multiple task sources, so that we can be sure that the tasks queued in a given task source are all executed in order. But modern browsers do indeed have multiple task queues. How they create these is a bit complex with a lot of edge cases, but the basic observable setup is that they’ll map the specs task sources to task queues.
So in this step, they are allowed to pick a task from whatever task source they see fit, and that’s where the prioritization system is done.
This means that only the tasks that have been queued to a task queue can be part of the prioritization system, so for instance, rendering frame callbacks (queued through
requestAnimationFrame
), orscroll
andresize
event handlers, etc. aren’t part of this prioritization system. These are to be called in the special update the rendering phase of the event loop, which is entered only once in a while (generally when the monitor sent its VSync signal).Microtasks are yet another beast and they are even less part of the prioritization system. The microtask queue is to be visited every time a script or a callback ends its execution, (if there is nothing on the JS stack anymore). There is no way that any other task slips in between, so there is no prioritization system in here.
Now if we come back to your example, we’re left with the task that will be queued by
setTimeout
, and the one that will be queued by the user interaction (UI).As we said before, the specs don’t define the prioritization, and the browsers will use their own heuristics to set it. And they may even use a starvation protection system, so that one task queue doesn’t block all the queues indefinitely. But what is observed for years in almost all browsers is that UI events (which will trigger your click handler callback) have an higher priority than timer (or even network) tasks. But since there is nothing defined, you shouldn’t rely on this behavior, and more importantly, it shouldn’t matter to you as a web-dev.
Note that we should soon be given the ability to control the priority of some task we’ll queue thanks to the Prioritized Task Scheduling API, (already available in Chromium browsers), which does expose three levels of prioritization: user-blocking (which roughly corresponds to UI events today), user-visible (basically timers and networks), and background (idle tasks). Maybe, when all this will be incorporated in HTML will all the different task sources have their own priority defined, maybe.