MutationObserver

Mutation Event

2000 年的时候引入了 Mutation Event, Mutation Event 采用了观察者的设计模式,当 DOM 有变动时就会立刻触发相应的事件,这种方式属于同步回调。

采用 Mutation Event 解决了实时性的问题,因为 DOM 一旦发生变化,就会立即调用 JavaScript 接口。但也正是这种实时性造成了严重的性能问题,因为每次 DOM 变动,渲染引擎都会去调用 JavaScript,这样会产生较大的性能开销。

MutationObserverAPI

MutationObserver 的作用是监控某个 DOM 节点

MutationObserver 将响应函数改成异步调用,可以不用在每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的 DOM 变化。这样即使频繁地操纵 DOM,也不会对性能造成太大的影响。

如果采用 setTimeout 创建宏任务来触发回调的话,那么实时性就会大打折扣,因为上面我们分析过,在两个任务之间,可能会被渲染进程插入其他的事件,从而影响到响应的实时性

MutationObserver 采用了“异步 + 微任务”的策略。

  • 通过异步操作解决了同步操作的性能问题
  • 通过微任务解决了实时性的问题

例子

 // 当观察到变动时执行的回调函数
    const callback = function (mutationsList, observer) {
      // Use traditional 'for loops' for IE 11
      for (let mutation of mutationsList) {
        if (mutation.type === "childList") {
          console.log("A child node has been added or removed.");
        } else if (mutation.type === "attributes") {
          console.log(
            "The " + mutation.attributeName + " attribute was modified."
          );
        }
      }
    };
 
    // 创建第一个 Observer
    const observer1 = new MutationObserver(callback);
 
    // Subject 目标对象
    const subject = document.getElementById("subject");
    
    // Observer 的配置(需要观察什么变动)
    const config = { attributes: true, childList: true, subtree: true };
 
    // Observer 订阅 Subject 的变化
    observer1.observe(subject, config);
 
    // 创建第二个 Observer
    const observer2 = new MutationObserver(callback);
 
    // Observer 订阅 Subject 的变化
    observer2.observe(subject, config);
 
    // Subject 的属性变化,会触发 Observer 的 callback 监听
    subject.className = "change class";
    
    // Subject 的子节点变化,会触发 Observer 的 callback 监听
    subject.appendChild(document.createElement("span"));
 
    // 这里为什么需要 setTimeout 呢?如果去除会有什么影响吗?
    setTimeout(() => {
      // 取消订阅
      observer1.disconnect();
      observer2.disconnect();
    });