In some cases, we want to be able to monitor changes in the DOM tree and do something about it. For example, listen for an element to be inserted into the DOM or removed from the DOM tree, and then animate it accordingly. Or you can enter special symbols in a rich text editor, such as a # or @ sign that automatically highlights what follows. To implement these capabilities, consider using the MutationObserver API. Next, Po will explore the power of the MutationObserver API.

By the end of this article, you will know the following:

  • What a MutationObserver is;
  • Basic use of the MutationObserver API and the MutationRecord object;
  • Detailed online use examples of the MutationObserver API configuration items;
  • Three common application scenarios for the MutationObserver API;
  • What is the Observer design pattern and how to implement the Observer design pattern using TS.

What is a MutationObserver

The MutationObserver interface provides the ability to monitor changes made to the DOM tree. It is designed as a replacement for the old Mutation Events feature, which is part of the DOM3 Events specification.

Using the MutationObserver API, we can monitor DOM changes. Any changes to the DOM, such as the addition or removal of nodes, changes in attributes, and changes in text content, are notified through the API.

MutationObserver has the following features:

  • It waits for all script tasks to complete before it runs, and it is triggered asynchronously. That is, it will wait until all the current DOM operations have finished before triggering, which is designed to deal with frequent DOM changes.
  • It encapsulates DOM change records into an array for unified processing, rather than processing them one by one.
  • It can either observe all types of CHANGES in the DOM or specify that only one type of change is observed.

Introduction to the MutationObserver API

Before introducing the MutationObserver API, let’s look at its compatibility:

(Photo credit: caniuse.com/#search=Mut…

As you can see from the figure above, the MutationObserver API is currently supported by most major Web browsers, but only IE 11 supports it. To use the MutationObserver API in a project, we first need to create a MutationObserver object, so let’s introduce the MutationObserver constructor.

The MutationObserver constructor in the DOM specification, which creates and returns a new observer that calls the specified callback function when the specified DOM event is fired. The MutationObserver observation of the DOM does not start immediately; instead, the observe() method must first be called to specify which DOM node to observe and which changes to respond to.

2.1 Constructors

The syntax of the MutationObserver constructor is:

const observer = new MutationObserver(callback);
Copy the code

The related parameters are described as follows:

  • Callback: a function that is called whenever a DOM change occurs to the specified node or subtree. The callback function takes two arguments: an array of MutationRecord objects that describe all the changes triggered, and a MutationObserver object from which the function was called.

Use the sample

const observer = new MutationObserver(function (mutations, observer) {
  mutations.forEach(function(mutation) {
    console.log(mutation);
  });
});
Copy the code

2.2 methods

  • Disconnect () : stops the MutationObserver instance from continuing to receive notifications. None of the observer object’s contained callback functions will be called until its observe() method is called again.

  • Observe (target[, options]) : This method is used to start a listener. It takes two arguments. The first parameter, which specifies the DOM node to observe. The second parameter, which is a configuration object, specifies the specific changes to be observed.

    const editor = document.querySelector('#editor');
    
    const options = {
      childList: true.// Monitor changes in the direct child nodes of node
      subtree: true.// Monitor all descendants of node for changes
      attributes: true.// Monitor node attributes for changes
      characterData: true.// Monitor changes in character data contained by nodes in the specified target node or child node tree.
      attributeOldValue: true // Record the old values of any changed attributes
    };
    
    observer.observe(article, options);
    Copy the code
  • TakeRecords () : Returns a list of all matching DOM changes that have been detected but not yet handled by the observer’s callback function, leaving the change queue empty. The most common use scenario for this approach is to get a record of all unprocessed changes immediately before disengaging the observer, so that any unprocessed changes can be processed when the observer is stopped.

2.3 MutationRecord object

Each time the DOM changes, a change record, called a MutationRecord instance, is generated. This instance contains all the information related to the change. The Mutation Observer object handles an array of individual MutationRecord instances.

The MutationRecord instance contains information about the change, with the following properties:

  • Type: The type of the change. The value can be Attributes, characterData, or childList;
  • Target: The DOM node that changed.
  • AddedNodes: Returns the added DOM nodes, or an empty NodeList if no nodes have been added;
  • RemovedNodes: Returns the removed DOM nodes, or an empty NodeList if no nodes have been removed;
  • PreviousSibling: Returns the sibling before the one that was added or removed, or if none was foundnull;
  • NextSibling: Returns the sibling after the added or removed node, or if none is presentnull;
  • AttributeName: returns the attributeName of the modified attribute, if setattributeFilterReturns only the pre-specified properties;
  • AttributeNamespace: Returns the namespace of the modified attribute;
  • OldValue: the value before the change. This property is only valid forattributecharacterDataThe change is effective if it occurschildListChanges, returnsnull.

2.4 Example of using MutationObserver

<! DOCTYPEhtml>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>DOM change observer example</title>
    <style>
      .editor {border: 1px dashed grey; width: 400px; height: 300px; }</style>
  </head>
  <body>
    <h3>DOM Mutation Observer</h3>
    <div contenteditable id="container" class="editor">Hello everyone, I am Brother A!</div>

    <script>
      const containerEle = document.querySelector("#container");

      let observer = new MutationObserver((mutationRecords) = > {
        console.log(mutationRecords); // Output the change record
      });

      observer.observe(containerEle, {
        subtree: true.// Monitor all descendants of node for changes
        characterDataOldValue: true.// Record the old value of any changed attribute
      });
    </script>
  </body>
</html>
Copy the code

> > < div style = “box-sizing: border-box; color: RGB (74, 74, 74); line-height: 22px; font-size: 14px! Important; white-space: inherit! Important;” Hello, everyone. I. For the above changes, the console will output five change records. Here is the last change record:

The observe(target [, options]) method of the MutationObserver object supports a number of configuration items that will not be covered here.

However, in order to give those new to the MutationObserver API a more intuitive sense of what each of the configuration items does, Apog has compiled the following online examples from the MutationObi-APi-Guide article:

1, MutationObserver Example – childList: codepen. IO /impressivew…

2, MutationObserver Example – childList with subtree: codepen. IO /impressivew…

3, MutationObserver Example – Attributes: codepen. IO /impressivew

4, MutationObserver Example Attribute Filter: codepen. IO /impressivew…

MutationObserver Example – ImpressiveFilter with subtree: codepen. IO /impressivew…

6, MutationObserver Example – characterData: codepen. IO /impressivew…

7, MutationObserver Example – characterData with Subtree: codepen. IO /impressivew…

8, MutationObserver Example – Recording an Old Attribute Value: codepen. IO /impressivew…

9, MutationObserver Example – Recording-Old characterData: codepen. IO /impressivew…

MutationObserver Example – Multiple Changes for a Single Observer: codepen. IO /impressivew…

9, MutationObserver Example – Moving a Node Tree: codepen. IO /impressivew…

Use scenarios of MutationObserver

3.1 Syntax highlighting

I believe you are not unfamiliar with grammar highlighting, usually reading all kinds of technical articles, will come across it. Next, Abo will show you how to use the MutationObserver API and the prisms.js library to implement JavaScript and CSS syntax highlighting.

Before we look at the implementation code, let’s look at the difference between syntax highlighting and syntax highlighting in the following HTML code segments:

let htmlSnippet = 
  let greeting =" hello, I'm A man ";  
       
 #code-container {border: 1px err grey; padding: 5px; }  

`
Copy the code

Looking at the figure above, you can see intuitively that code blocks with syntax highlighting are much clearer to read. Let’s look at the code that implements syntax highlighting:

<! DOCTYPEhtml>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>MutationObserver practical syntax highlighting</title>
    <style>
      #code-container {
        border: 1px dashed grey;
        padding: 5px;
        width: 550px;
        height: 200px;
      }
    </style>
    <link href="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/themes/prism.min.css" rel="stylesheet">
    <script src="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/prism.min.js" data-manual></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/components/prism-javascript.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/components/prism-css.min.js"></script>
  </head>
  <body>
    <h3>Abo: Grammar highlighting in action in MutationObserver</h3>
    <div id="code-container"></div>
    <script>
      let observer = new MutationObserver((mutations) = > {
        for (let mutation of mutations) {
          // Get the new DOM node
          for (let node of mutation.addedNodes) {
            // Only handle HTML elements and skip other nodes, such as text nodes
            if(! (nodeinstanceof HTMLElement)) continue;

            // Check whether the inserted node is a code segment
            if (node.matches('pre[class*="language-"]')) {
              Prism.highlightElement(node);
            }

            // Check whether the children of the inserted node are code segments
            for (let elem of node.querySelectorAll('pre[class*="language-"]')) { Prism.highlightElement(elem); }}}});let codeContainer = document.querySelector("#code-container");

      observer.observe(codeContainer, { childList: true.subtree: true });
      // Insert content with code snippet dynamically
      codeContainer.innerHTML = 
 let greeting =" hello, I'm A man "; < div> 
 #code-container {border: 1px err grey; padding: 5px; }  

`;
</script>
</body>
</html>
Copy the code

In the above code, we first set the data-manual attribute on the script tag that introduced prism.min.js to tell prism.js that we are going to handle syntax highlighting in manual mode. We then further obtain the added DOM nodes in the callback function by obtaining the addedNodes attribute of the mutation object. We then iterate over the new DOM node to determine whether the new DOM node is a code segment, and highlight it if it is.

In addition, in addition to determining the current node, we also determine whether the children of the inserted node are code segments and highlight them if the conditions are met.

3.2 Listening for Load or Unload Events of elements

For Web developers, many are familiar with the Load event. The Load event is triggered when the entire page and all dependent resources, such as style sheets and images, have been loaded. The Unload event is triggered when a document or a child resource is being unloaded.

In daily development, in addition to listening for page load and unload events, we often need to listen for DOM node insertion and removal events. For example, an insert animation is generated when a DOM node is inserted into the DOM tree, and a remove animation is generated when the node is removed from the DOM tree. For this scenario, we can use the MutationObserver API to listen for the addition and removal of elements.

Again, before looking at the actual implementation code, let’s look at the actual effect:

In the above example, when the Trace element lifecycle button is clicked, a new DIV element is inserted into the body, and when successfully inserted, the relevant information is displayed in the message box. After 3S, the newly added DIV element will be removed from the DOM. After successful removal, the information that the element has been removed from the DOM will be displayed in the message box.

Let’s take a look at the implementation:

index.html

<! DOCTYPEhtml>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>MutationObserver load/unload event</title>
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.0.0/animate.min.css"
    />
  </head>
  <body>
    <h3>Abo: MutationObserver Load/Unload event</h3>
    <div class="block">
      <p>
        <button onclick="trackElementLifecycle()">Track the element lifecycle</button>
      </p>
      <textarea id="messageContainer" rows="5" cols="50"></textarea>
    </div>
    <script src="./on-load.js"></script>
    <script>
      const busy = false;
      const messageContainer = document.querySelector("#messageContainer");

      function trackElementLifecycle() {
        if (busy) return;
        const div = document.createElement("div");
        div.innerText = "I'm the new DIV element.";
        div.classList.add("animate__animated"."animate__bounceInDown");
        watchElement(div);
        document.body.appendChild(div);
      }

      function watchElement(element) {
        onload(
          element,
          function (el) {
            messageContainer.value = "The element has been added to the DOM, will be removed in 3s.";
            setTimeout(() = > document.body.removeChild(el), 3000);
          },
          function (el) {
            messageContainer.value = "The element has been removed from the DOM"; }); }</script>
  </body>
</html>
Copy the code

on-load.js

// Contains only part of the code
const watch = Object.create(null);
const KEY_ID = "onloadid" + Math.random().toString(36).slice(2);
const KEY_ATTR = "data-" + KEY_ID;
let INDEX = 0;

if (window && window.MutationObserver) {
  const observer = new MutationObserver(function (mutations) {
    if (Object.keys(watch).length < 1) return;
    for (let i = 0; i < mutations.length; i++) {
      if (mutations[i].attributeName === KEY_ATTR) {
        eachAttr(mutations[i], turnon, turnoff);
        continue;
      }
      eachMutation(mutations[i].removedNodes, function (index, el) {
        if (!document.documentElement.contains(el)) turnoff(index, el);
      });
      eachMutation(mutations[i].addedNodes, function (index, el) {
        if (document.documentElement.contains(el)) turnon(index, el); }); }}); observer.observe(document.documentElement, {
    childList: true.subtree: true.attributes: true.attributeOldValue: true.attributeFilter: [KEY_ATTR],
  });
}

function onload(el, on, off, caller) {
  on = on || function () {};
  off = off || function () {};
  el.setAttribute(KEY_ATTR, "o" + INDEX);
  watch["o" + INDEX] = [on, off, 0, caller || onload.caller];
  INDEX += 1;
  return el;
}
Copy the code

On – load. The integrity of the js code, gist.github.com/semlinker/a…

3.3 Rich text editor

In addition to the first two application scenarios, the MutationObserver API also has its place in the rich text editor scenario. For example, if we want to highlight the content after the # symbol in a rich text editor, we can use the MutationObserver API to listen for user input and find that the input is automatically highlighted when the user enters a # symbol.

Here we use the vue-Hashtag-Textarea project to demonstrate the effect:

In addition, the MutationObserver API is used in a Github project called editor.js. Editor.js is a block-Styled Editor, rich text and media Editor for the output of data in JSON format. It is completely modular, consisting of “blocks,” meaning that each unit of structure is its own block (paragraphs, headings, images are blocks, for example), and users can easily write their own plug-ins to further extend the editor.

Inside the editor.js Editor, it listens for changes in the content of the rich text box through the MutationObserver API, and then fires the change event, allowing the outside world to respond to and process the changes. The above functions are encapsulated in the internal ModificationSobServer. ts module, interested partners can read the modificationSobServer. ts module code.

Of course, with the power provided by the MutationObserver API, there are other scenarios where watermarking elements on a page can be removed to prevent untraceable “leaker” elements. This is not absolute security, of course, but just an extra layer of protection. How to delete the watermark element is limited in length. Here a baoge does not continue to expand the introduction, you can refer to the nuggets on the “open the console can not delete elements, front-end are scared urine” this article.

Now that MutationObserver is done, I can’t help but talk about the observer design pattern.

The observer design pattern

4.1 introduction

Observer mode, which defines a one-to-many relationship in which multiple observer objects listen to a topic object at the same time. When the topic object’s state changes, all observer objects are notified so that they can automatically update themselves.

We can use the example of a daily journal subscription to visualize the above concept. Journal subscriptions involve two main roles: the journal publisher and the subscriber, and the relationship between them is as follows:

  • Periodical publisher — responsible for the publication and distribution of periodicals.
  • Subscribers – Simply subscribe to a new edition of the journal and will be notified when it is published. If you unsubscribe, you will not receive any further notifications.

There are also two main roles in the Observer pattern: Subject and Observer, which correspond to the journal publisher and subscriber in the example, respectively. Let’s take a look at a picture to further understand these concepts.

4.2 Pattern Structure

The observer mode contains the following roles:

  • Subject: Subject class
  • -Blair: I’m an Observer.

4.3 Observer mode combat

4.3.1 Defining the Observer Interface
interface Observer {
  notify: Function;
}
Copy the code
4.3.2 Create ConcreteObserver implementation class
class ConcreteObserver implements Observer{
    constructor(private name: string) {}

    notify() {
      console.log(`The ${this.name} has been notified.`); }}Copy the code
4.3.3 Create the Subject class
class Subject { 
    private observers: Observer[] = [];

    public addObserver(observer: Observer): void {
      console.log(observer, "is pushed!");
      this.observers.push(observer);
    }

    public deleteObserver(observer: Observer): void {
      console.log("remove", observer);
      const n: number = this.observers.indexOf(observer); n ! = -1 && this.observers.splice(n, 1);
    }

    public notifyObservers(): void {
      console.log("notify all the observers".this.observers);
      this.observers.forEach(observer= >observer.notify()); }}Copy the code
4.3.4 Example
const subject: Subject = new Subject();
const semlinker = new ConcreteObserver("semlinker");
const kaquqo = new ConcreteObserver("kakuqo");
subject.addObserver(semlinker);
subject.addObserver(kaquqo);
subject.notifyObservers();

subject.deleteObserver(kaquqo);
subject.notifyObservers();
Copy the code

After the above code runs successfully, the console will output the following:

[LOG]: { "name": "semlinker" },  is pushed! 
[LOG]: { "name": "kakuqo" },  is pushed! 
[LOG]: notify all the observers,  [ { "name": "semlinker" }, { "name": "kakuqo" } ] 
[LOG]: semlinker has been notified. 
[LOG]: kakuqo has been notified. 
[LOG]: remove,  { "name": "kakuqo" } 
[LOG]: notify all the observers,  [ { "name": "semlinker" } ] 
[LOG]: semlinker has been notified. 
Copy the code

By observing the above output, when the observer is removed, subsequent notifications are not received. The Observer pattern supports simple broadcast communication, automatically notifting all subscribed objects. But if an observed object has many observers, notifying all observers can take a long time. So in the actual project, we need to pay attention to the above problems.

5. Reference Resources

  • MDN – MutationObserver
  • MDN – MutationRecord
  • JavaScript Standard Reference Tutorial – MutationObserver
  • mutationobserver-api-guide
  • javascript.info-mutation-observer