Search code examples
javascriptgoogle-chromeeventsdom-eventsjavascript-engine

Macrotasks and microtasks exercise


I have the following question about micro and macro tasks running in Google Chrome. See the next example (The following code was proposed by Jake Archibald):


// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the
// outer element
new MutationObserver(function () {
  console.log('mutate');
}).observe(outer, {
  attributes: true,
});

// Here's a click listener…
function onClick() {
  console.log('click');

  setTimeout(function () {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function () {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

inner.click();

If you run the following code, the order of logs is:

click
click
promise
mutate
promise
timeout
timeout

There are two questions about what is happening here that I don't understand, for both I have a vague idea.

  1. Why click is print twice if inner.click() is called once? I think that it is because click inner makes click outer too, but I am not sure.
  2. Why mutate changes only once? It makes sense to me because in the second iteration it is already mutated so it doesn't trigger the callback.

I am not sure in none of them.

Any ideas? Thanks, Manuel


Solution

  • Why click is print twice if inner.click() is called once? I think that it is because click inner makes click outer too, but I am not sure.

    Yes - it's because the click event propagates upward to containing elements. Unless stopPropagation is called on an event, it will trigger all listeners between the element that the event was dispatched to (here, the inner) and the root. The outer is a container of inner, so clicks on inner make their way to outer while propagating.

    Why mutate changes only once? It makes sense to me because in the second iteration it is already mutated so it doesn't trigger the callback.

    Because both mutations of the DOM were done synchronously, when the setAttribute was called in both click listeners - the observer only runs after all listener tasks are complete.

    Both mutations will be visible in the mutation list (the array passed to the MutationObserver callback):

    // Let's get hold of those elements
    var outer = document.querySelector('.outer');
    var inner = document.querySelector('.inner');
    
    // Let's listen for attribute changes on the
    // outer element
    new MutationObserver(function (mutations) {
      console.log('mutate');
      for (const mutation of mutations) {
        console.log('change detected on attribute', mutation.attributeName);
      }
    }).observe(outer, {
      attributes: true,
    });
    
    // Here's a click listener…
    function onClick() {
      console.log('click');
    
      setTimeout(function () {
        console.log('timeout');
      }, 0);
    
      Promise.resolve().then(function () {
        console.log('promise');
      });
    
      outer.setAttribute('data-random', Math.random());
    }
    
    // …which we'll attach to both elements
    inner.addEventListener('click', onClick);
    outer.addEventListener('click', onClick);
    
    inner.click();
    <div class="outer">
      <div class="inner">
        click
      </div>
    </div>

    For similar reasons:

    elm.className = 'foo';
    elm.className = 'bar';
    

    will result in an observer on elm firing only once.

    In contrast

    elm.className = 'foo';
    setTimeout(() => {
      elm.className = 'bar';
    });
    

    (or the same sort of thing implemented in your code) will result in the observer firing twice, because the timeout is a macrotask and the observer callback runs as a microtask.