preface

Recently, I came into contact with MutationObserver again in the re-learning oF JavaScript, and then I remembered that this interface was used in Vue source code, so I felt it was necessary to understand and learn MutationObserver interface.

Here is the code for using the MutationObserver interface in the vue source code:

MutationObserver

The main role

MutationObserver can observe the entire document, a part of the DOM tree, or a specific DOM element, primarily for changes to the element’s attributes, child nodes, and text, and can perform asynchronous callbacks when the DOM is modified.

The MutationObserver interface is intended to replace the deprecated MutationEvent:

  • Mutationevents, described in the DOM Level 2 specification, define a set of events that are triggered when various DOM changes occur. This interface suffers from serious performance issues due to the implementation mechanism of browser events. Therefore, DOM Level 3 rules deprecate these events. – The MutationObserver interface is more practical and provides better performance

Basic usage

An instance of MutationObserver is created by calling the MutationObserver constructor and passing in a callback function that takes two arguments:

  • mutationRecordAn array stores an instance of a MutationRecord, and each entry in the array contains what changed and what part of the DOM was affected. Because more than one condition that meets the dom modification being observed may occur simultaneously before the callback is executed, the current callback is executed multiple times, each time passing an array of MutationRecord instances that are enqueued in order;
  • mutationObserver— is a MutationObserver instance to observe changes, i.e., externally instantiated observer objects;
 let observer = new MutationObserver((mutationRecord, mutationObserver) = > {
            console.log('DOM was mutated! ');
        });
        
console.log("observer = ", observer);
Copy the code

And you getobserveInstances can callMutationObserverThree methods on the prototype:

  • observe()
  • disconnect()
  • takeRecords()

MutationObserverInit object

Before we introduce these methods, it’s important to look at the MutationObserverInit object, because the second argument to the Observe () method needs to receive a MutationObserverInit object.

Observe the observerInit object. Observe the observerInit object. Observe the observerInit object.

  • Attribute changes, such as: dom removeAttribute () | | dom. The setAttribute (), etc
  • Text changes, such as: the dom. The innerText = XXX | | the dom. The innerHTML = XXX | | dom. TextContent = XXX, etc
  • Child node changes, such as: the dom. The appendChild () | | dom. The insertBefore () | | dom. The replaceChild () | | dom. RemoveChild (), etc

Properties of the MutationObserverInit object, whose values are Boolean except for the array value of the attributeOldValue attribute:

  • Subtree — true indicates that changes in child nodes need to be detected, false indicates the opposite
  • Attributes — true indicates that attribute changes need to be detected, false indicates the opposite
  • AttributeFilter — An array of strings that indicate which attributes to observe for changes
  • AttributeOldValue — true indicates that the MutationRecord needs to record the value of the attribute before it changes. False is the opposite. Once this attribute is set to true, the value of the attributes is set to true
  • CharacterData — true indicates whether modifying the text content triggers the change event, false indicates the opposite
  • CharacterDataOldValue — true means that MutationRecord needs to record the characterData before it changed. False, in contrast, sets the characterData value to true once this property is set to true
  • ChildList — indicates whether the children of the modified target node trigger the change event, false does the opposite

In summary, an object with properties that match those defined on MutationObserverInit can be called a MutationObserverInit object

When calling observe(), At least one of attribute, characterData, childList, or A ttributeOldValue, characterDataOldValue in the MutationObserverInit object must be true. Otherwise, an error is thrown because no change event can trigger the callback, but the callback is registered.

Observe () method

Associate the Observer with the DOM

A newly created MutationObserver instance does not associate any part of the DOM. You must associate the OBSERVER with the DOM through observer.observe().

Observer.observe (DOM, mutationObserverInit) two required parameters: observer.observe(dom, mutationObserverInit)

  • dom— The DOM node to watch change
  • mutationObserverInit— Objects that match the definition of MutationObserverInit

The following is an example of observing an attribute change on the tag:

// Instantiate the Observer and register the callback
 let observer = new MutationObserver((mutationRecord, mutationObserver) = >{
          // About 2s executes this callback
          console.log('body attributes changed!!! '); // body attributes changed!!!
          console.log('mutationRecord = ', mutationRecord); // [MutationRecord]
          console.log('mutationObserver === observer', mutationObserver === observer);// true
         });
         
 // Associate the Observer instance with the target DOM
 observer.observe(document.body, { attributes: true });
 
 // Modify the class value of the body tag after about 2s
   setTimeout(() = > {
        document.body.setAttribute('class'.'body')},2000)
Copy the code

The MutationRecord instance in the callback function

Console. log(‘mutationRecord = ‘, mutationRecord) produces the following output:

 mutationRecord =  [
                          {
                              addedNodes: NodeList [],
                              attributeName: "class".attributeNamespace: null.nextSibling: null.oldValue: null.previousSibling: null
                              removedNodes: NodeList [],
                              target: body.body
                              type: "attributes"}]Copy the code

Here is the explanation for each attribute:

  • Target – The target DOM node affected by the modification
  • Type — Represents the type of change, namely the three types in the MutationObserverInit object: “Attributes”, “characterData”, or “childList”
  • AttributeName – Saves the name of the modified attribute for changes to the “Attributes” type
  • AttributeNamespace — For changes of type “Attributes” that use namespaces, save the name of the modified attribute, which is set to NULL by other change events
  • OldValue — If enabled in the MutationObserverInit object (attributeOldValue or characterData oldValue is true), The “Attributes” or “characterData” change event sets this property to the substituted value; Changes to the childList” type always set this property to null
  • AddedNodes — For changes of type “childList”, return the NodeList containing the node added in the change. Other change events set this property to an empty NodeList array
  • PreviousSibling — For a change of type “childList”, return the NodeList containing the node deleted in the change, default empty NodeList
  • NextSibling — for changes of type “childList”, return the nextSibling of the changed Node, null by default
  • RemovedNodes — For changes of type childList, return the previous sibling of the changed Node, null by default

Disconnect () method

By default, as long as the observed element is not garbage collected, the MutationObserver callback is executed in response to the DOM change event. To terminate the execution of the callback early, call the disconnect() method.

Look directly at the following example:

            let observer = new MutationObserver((mutationRecord, mutationObserver) = > {
                console.log(mutationRecord);
                console.log(mutationObserver);
            })

            observer.observe(document.body, { attributes: true });
            
            / / position 1
            observer.disconnect();
            
            setTimeout(() = > {
              / / position 2
              // observer.disconnect();
              
              document.body.setAttribute('class'.'body');
              
              / / position 3
              // observer.disconnect();
            }, 2000);
            
           / / position 4
           // observer.disconnect();
Copy the code

Above, we put observer.disconnect() at positions 1, 2, 3, and 4, respectively, but in fact, they all have the same effect, terminating the callback directly. To enable the callback that has joined the task queue to execute, you can use the event loop mechanism, such as: Distinguish between synchronous and asynchronous modifications, and then call Disconnect () in the asynchronous operation to ensure that the callback that has been queued completes. For information on the browser’s EventLoop mechanism, see my previous article JavaScript EventLoop — Browser & Node

TakeRecords () method

Calling the takeRecords() method of a MutationObserver instance emptens the record queue, fetching and returning an array of all MutationRecord instances within it.

Usage scenario: You want to disconnect from the observation target, but you also want to get the MutationRecord in the record queue discarded by the call disconnect(), so that subsequent operations can continue even after disconnection.

     // 1. TakeRecords () is not called
     let observer = new MutationObserver(
        (mutationRecord, mutationObserver) = > {
          console.log('body had mutated!!! ')
          console.log(mutationRecord); // [MutationRecord, MutationRecord, MutationRecord]
        },
      )

      observer.observe(document.body, { attributes: true })

      document.body.className = 'body1'
      document.body.className = 'body2'
      document.body.className = 'body3'
      
     // 2. TakeRecords ()
     let observer = new MutationObserver(
     // This callback is no longer executed because mutationRecord has been obtained through observer.takerecords
        (mutationRecord, mutationObserver) = > {
          console.log('body had mutated!!! ')
          console.log(mutationRecord); // [MutationRecord, MutationRecord, MutationRecord]
        },
      )

      observer.observe(document.body, { attributes: true })

      document.body.className = 'body1'
      document.body.className = 'body2'
      document.body.className = 'body3'
      
      console.log(observer.takeRecords()); [MutationRecord, MutationRecord, MutationRecord]
      console.log(observer.takeRecords());  [] [] [] [] [] []
Copy the code

Reuse MutationObserver objects

By calling the observe() method multiple times, you can reuse a MutationObserver to observe multiple different target nodes. At this point, the target property of the MutationRecord can identify the target node of the changed event.

      let h1 = document.createElement('h1')
      let h2 = document.createElement('h2')

      let observer = new MutationObserver(
        (mutationRecord, mutationObserver) = > {
          console.log(mutationRecord)
        },
      )
      // First check
      observer.observe(h1, {
        attributes: true,})// Check again
      observer.observe(h2, {
        attributes: true,
      })

      h1.className = 'h1'
      h1.textContent = 'this is h1'
      h2.className = 'h2'
      h2.textContent = 'this is h2'

     // The above changes to className can trigger callback execution even if the h1 and H2 nodes are not added to the document
      document.body.appendChild(h1)
      document.body.appendChild(h2)
Copy the code

Observer.disconnect () disconnects all DOM connections to the Observer after the observer.disconnect() method is called, but you can continue to reconnect using observer.observe().

MutationObserver callback and record queue

The MutationObserver interface is designed for performance, with asynchronous callbacks and record queues at its core. In order not to affect performance when a large number of change events occur, information about each change (determined by the Oberver instance) is saved in the MutationRecord instance and then added to the record queue.

The record queue is unique to each MutationObserver instance and is an ordered list of all DOM change events.

The body element is modified twice in a row, but the registered callback function is not executed twice. Instead, the information from the two operations is stored in an array in an instance of MutationRecord. This ensures that multiple changes can be retrieved in a single callback execution.

// Instantiate the Observer and register the callback
let observer = new MutationObserver((mutationRecord, mutationObserver) = > {
   console.log(mutationRecord);// The output is a set of two changes
})
// Associate the observer with the DOM
observer.observe(document.body, { attributes: true });
// Change the property value twice
document.body.className = "body1";
document.body.className = "body2";
Copy the code

There is still a cost to using MutationObserver

Although many advantages of MutationObserver have been mentioned above, it should be understood as compared to the old MutationEvent, because MutationObserver itself still has disadvantages. This is why it is not directly used in the VUE source code, and of course it is second only to Promises in VUE, because MutationObserver, like Promises, is a microtask that can be executed as quickly as possible by an event loop.

  • MutationObserver references
    • The MutationObserver reference to the target node to observe is a weak reference, so it does not prevent the garbage collector from collecting the target node
    • The target node’s reference to MutationObserver is a strong reference. If the target node is removed from the DOM and subsequently garbage collected, the associated MutationObserver is also garbage collected.
  • MutationRecord references
    • Each MutationRecord instance in the record queue contains at least one reference to an existing DOM node, the target property, or multiple node references if the change is of type childList
    • The default behavior for record queue and callback processing is to exhaust the queue, process each MutationRecord, and then let them go out of scope and be garbage collected
    • Sometimes it may be necessary to keep a complete record of changes for an observer, so saving all instances of MutationRecord will save the nodes they reference, which will prevent these nodes from being recycled
    • If you need to free up memory as quickly as possible, you can extract the most useful information from each MutationRecord, save it to a new object, and then release the references in the MutationRecord

The last

The vue2 source code uses MutationObserver, which is also related to nextTick source code.

  • Start by defining a callbacks to hold all the callbacks in nextTick
  • Then determine whether the current environment supports promises, and if so, use the Promise to trigger the callback function
  • If promises are not supported, MutationObserver is supported or not, and all asynchronous callback functions are triggered to execute by watching the text node change
  • SetImmediate is used to trigger the callback function if MutationObserver is not supported
  • If none of the above is supported, only setTimeout can be used for asynchronous execution

Deferred call priorities are as follows:

Promise > MutationObserver > setImmediate > setTimeout