1

I don't understand why mutation observer callbacks or promises are prioritized using microtask queues.

Things like user clicks, etc., are processed with the macro task queue. Why was this decision made? Isn't a user action like clicking much more essential to handle than a developer action like a promise?

Is there any concrete explanation or documentation on this? Or is this just part of the evolution of JS when JS was created? There were very few general-purpose computers with multiple cores, so no one gave much thought, and later, we were stuck with the behavior of legacy support.

6
  • I think it would help if you could link to your actual source for this claim(s), just to set up some more precise context to answer the question.
    – Pac0
    Commented Mar 18 at 16:57
  • 1
  • 1
    not a downvoter, but imho questions on design rationale of languages are in a greyline for Stack Overflow, often frowned upon. See for instance this meta answer from Eric Lippert.
    – Pac0
    Commented Mar 18 at 18:13
  • Thanks, @Pac0, But I am not asking for a design rationale. I genuinely want to understand why someone would prioritize a developer event over a user event. I am unable to find an answer to this. Can you help me re-ask the question to get a satisfactory answer?
    – soumitra
    Commented Mar 18 at 18:52
  • 1
    Usually the behaviour is that a macrotask schedules multiple microtasks (e.g. when resolving a promise from a click handler), and then you want those microtasks to be processed as soon as possible. They are meant to be immediate, just not synchronous. The next event that causes the next macrotask will happen much later anyway.
    – Bergi
    Commented Mar 18 at 20:12

1 Answer 1

2

The MutationObserver's callback should be executed before updated DOM's rendering which happens in the end of a macrotask, otherwise if you update DOM in the callback there will be a flicker/screen tearing since the callback will be called AFTER the rendering, in another macrotask.

On the other hand you can change DOM sync with many mutations. If MutationObserver would be sync that would lead to excessive amount of events plus possibly additional mutations in the callback leading to infinite recursion.

Sometimes you need to fight the recursion in the callback even with the async nature of MutationObserver.

Here we mutate the style attribute 2 times. Imagine if the amount of props would be bigger and each prop triggers MutationObserver.

Consider when you in the callback the DOM is "settled" in the sync phase of the macrotask. So you have more control of what happens later.

In the code below it's clear that the observe callback is queued sync along with the other microtasks. If you move the mouse cursor mostly horizontally then the first mutation happens and the observe callback is scheduled immediately. Further DOM mutations just add mutations to the mutation list passed to the callback.

So back to our callback, check the black marks on X and Y axis, we mutate their style on the observe callback. If the callback is executed in another macrotask the marks would be out of sync with the pointer circle because the marks and the pointer would be rendered separately in their synced state and there could be additional mutations to the pointer between the macrotasks.

document.addEventListener('mousemove', e => {

  queueMicrotask(()=>console.log('microtask before mutations'));
  $pointer.style.left = e.x + 'px';
  console.log('left mutated');
  queueMicrotask(()=>console.log('microtask between mutations'));
  $pointer.style.top = e.y + 'px'
  console.log('top mutated');
  queueMicrotask(()=>console.log('microtask after mutations'));
  
});

new MutationObserver(()=>{
  console.log(`moved ${$pointer.style.left} ${$pointer.style.top}`);
  const {left, top} = $pointer.getBoundingClientRect();
  $xaxis.style.left = left + 'px';
  $yaxis.style.top = top + 'px';
  
  
}).observe($pointer, {attributes:true});
.pointer{
 width:16px;
 height:16px;
 border: 1px solid gray;
 border-radius:50%;
 position: fixed;
 margin: -8px 0 0 -8px;
}

.axis-mark{
   width:16px;
    height:16px;
    background:black;
    left: 0;
    top:0;
    position: fixed;
}
<div class="pointer" id="$pointer"></div>
<div class="axis-mark" id="$xaxis"></div>
<div class="axis-mark" id="$yaxis"></div>

5
  • will there be difference if mutation observers went to task queue instead of micro task queue?
    – soumitra
    Commented Mar 18 at 18:59
  • @soumitra i think there's no difference, a microtask is a microtask. added microtask to the code, and updated the answer Commented Mar 18 at 19:02
  • There is a difference in priority between microtask and task queue; the explanation for the behavior of the order of logs seems to be that the mutation observer sends the callback when the first mutation occurs and waits for it to be handled before sending the next mutation.
    – soumitra
    Commented Mar 18 at 19:17
  • @soumitra not sure i follow you, i wasnt talking about tasks Commented Mar 18 at 19:26
  • I got the answer, I think, @Alexander, I think the idea of a microtask queue is to allow a callback to be executed after each DOM change, as in the case of your example, when the mouseover event is called, it changes the DOM nodes, those take effect, and we need a callback immediately. This behavior is only possible with microtask queues and not with task queues. Also, what if I cause changes from the mutation observer? I want to react to them ASAP. Otherwise, we can have other mutations.
    – soumitra
    Commented Mar 18 at 19:39

Not the answer you're looking for? Browse other questions tagged or ask your own question.