This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!

preface

Browsers “animate” pages through an event loop in which macrotasks and microtasks execute at different times, and the browser’s microtask-based technologies include MutationObserver, Promise, and many other technologies developed on the basis of promises.

The previous article spent a lot of time introducing Promises, so this article takes a look at what the MutationObserver microtask is. What to do!

MutationObserver is a set of methods for listening to DOM changes, which has been a very core requirement for front-end engineers. For example, many Web applications use HTML and JavaScript to build their own custom controls, which, unlike some built-in controls, are not inherent. To work well with the built-in controls, they must be able to accommodate content changes, respond to events, and user interactions. Therefore, Web applications need to monitor DOM changes and respond to them in a timely manner.

MutationObserver is now a way to listen for DOM changes, but how did it start? Understanding the evolution of listening to DOM methods can help us understand how browsers work.

Early polling detection

In the early days, browsers didn’t provide support for listening to the DOM, so the only way to see if the DOM was changing was to do polling checks, such as using setTimeout or setInterval to periodically check if the DOM was changing.

This approach is simple and crude, but it runs into two problems:

  • If the time interval is set too long, the DOM will not respond to changes in time.
  • On the other hand, if the time interval is set too short, you will waste a lot of useless work checking the DOM, making the page inefficient.

Mutation Event

In 2000, Mutation events, defined in DOM3 to listen for changes in the STRUCTURE of the DOM tree, were introduced, but have been deprecated due to compatibility and performance issues.

There are 7 types of Mutation events in total: DOMNodeInserted, DOMNodeRemoved, DOMSubtreeModified, DOMAttrModified, DOMCharacterDataModified, DOMNodeInsertedIntoDocument And DOMNodeRemovedFromDocument.

Simple usage is as follows:

let box = document.getElementById('box')
box.addEventListener("DOMSubtreeModified".function () {
  console.log('Box element modified');
}, false);
Copy the code

The Mutation Event adopts the design mode of observer. When the DOM changes, the corresponding Event will be triggered immediately, which is a synchronous callback.

The Mutation Event solves the real-time problem, because the JavaScript interface is called as soon as the DOM changes. However, this real time creates serious performance problems, as the rendering engine calls JS every time the DOM changes, which creates a significant performance overhead.

For example, if 50 nodes are dynamically created or modified using JS, 50 callbacks will be triggered, and each callback function needs a certain execution time. Here, we assume that the execution time of each callback is 4ms, then the execution time of 50 callbacks is 200ms. If the browser is executing an animation effect at this time, Because the Mutation Event triggers the callback Event, it causes the animation to stall.

It is precisely because of the page performance problems caused by the use of Mutation Events that they were deprecated and gradually removed from Web standard events.

MutationObserver

The MutationObserver API can be used to monitor DOM changes, including property changes, node changes, content changes, and so on.

MutationObserverThe use of

Refer to the MutationObserver for the MDN documentation

MutationObserver is a constructor that instantiates a Mutation observer object. The argument is a callback function that is executed after the specified DOM node has sent changes. The callback function takes two arguments:

  • mutations: Array of node change records (MutationRecord)
  • observer: The observer object itself
let observe = new MutationObserver(function (mutations, observer) {});
Copy the code

The MutationObserver instance object has three methods, as follows:

  • observeConfiguration:MutationObserverWhen the DOM changes to match a given option, it begins to receive notification through its callback function. That is, the observation object is set and two parameters are accepted:
    • target: Observe the target;
    • options: Sets viewing options through object members
  • disconnect: stopMutationObserverThe instance continues to receive the notification until it is called againobserve()Method, the observer object contains callback functions that are never called again.
  • takeRecordsFrom:MutationObserverDelete all pending notifications from the notification queue and return them toMutationRecordThe object of newArrayIn the. Empty the queue of records and return the contents.

Example:

// Select the node to observe changes
const targetNode = document.getElementById('box');
// The configuration of the observer (what changes to observe)
const config = {
  attributes: true.childList: true.subtree: true
};
// The callback function to execute when a change is observed
const callback = function (mutationsList, observer) {
  for (let mutation of mutationsList) {
    if (mutation.type === 'childList') {
      console.log('A node has changed, the current node contains:' + mutation.target.innerHTML);
    } else if (mutation.type === 'attributes') {
      console.log('Modified' + mutation.attributeName + 'properties'); }}};// Create an observer instance and pass in the callback function
const observer = new MutationObserver(callback);
// Start observing the target node with the above configuration
observer.observe(targetNode, config);
// After that, you can stop observing
// observer.disconnect();
Copy the code

MutationObserverImproved optimization of

  • First, the MutationObserver changes the response function to an asynchronous call, so that instead of firing the asynchronous call every time the DOM changes, it fires the asynchronous call once, after several DOM changes, and uses a data structure to record all DOM changes during that time. This way, even with frequent DOM manipulation, performance is not affected too much.
  • Each time a DOM node changes, the rendering engine encapsulates the change record as a microtask and adds the microtask to the current microtask queue. When the checkpoint is reached, the V8 engine will execute the microtasks in sequence.

To sum up, MutationObserver adopts an asynchronous + microtask strategy to monitor DOM changes.

  • The performance problem of synchronous operation is solved by asynchronous operation.
  • The real-time problem is solved by micro-task.

MutationObserverAnd in the VuenextTick

NextTick in Vue allows us to perform a delayed callback after the end of the next DOM update loop to get the updated DOM.

So how is nextTick implemented in Vue?

Vue executes asynchronously when updating the DOM. As soon as data changes are listened for, Vue opens a queue and buffers all data changes that occur in the same event loop. If the same watcher is fired multiple times, it will only be pushed into the queue once. This removal of duplicate data at buffering time is important to avoid unnecessary computation and DOM manipulation. Then, in the next event loop “tick”, Vue refreshes the queue and performs the actual (deduplicated) work.

We know that there are macrotasks and microtasks for asynchronous callbacks. In order to make nextTick execute faster, microtasks will be preferred. To create a new microtask, use the Promise first and try MutationObserver again if the browser does not support it. If not, use the setTimeout macro task.

The asynchronous update queue in Vue says:

The MutationObserver creates a TextNode, listens for changes, and then changes the text of the node when the nextTick is required:

    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () = > {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
Copy the code

conclusion

This article describes the evolution of technical solutions for monitoring DOM changes, from polling to Mutation Events to the latest use of MutationObserver. The core of the MutationObserver scheme is the use of microtask mechanism to effectively balance the real-time and efficiency of execution.

Finally, the relationship between MutationObserver and nextTick in Vue is briefly introduced.