作为前端开发,你了解MutationObserver吗?

本文最后更新于:6 个月前

目录

前言

演变过程

基础概念

MutationObserver

observe(target, options)

attributes:是否监听标签属性变化

childList:是否监听子节点变化

characterData:是否监听文本节点内容的变化

attributeOldValue:是否记录属性变化前的值

characterDataOldValue:是否记录文本节点内容变化前的值

subtree:是否监听后代节点变化

attributeFilter:过滤属性名称

disconnect()

takeRecords()

MutationRecord[]

MutationRecord的属性

MutationObserver的应用场景

检测DOM变化并做出响应

动态样式变化

标签之间通信

缺点

首先是性能损耗

其次是操作冲突

最后是无法在IFrame中监听变化

总结


前言

MutationObserver在开发中或许不常使用,但是特殊情况下确实可以解决某些问题。它与addEventListener有些类似,当用户触发了某些事件操作时会调用对应的回调

前些天在需求迭代中使用到了MutationObserver,由于Antd早期版本的弹窗没有做响应功能,以及代码中的弹窗许多没有进行二次封装,导致无法得知弹窗何时出现及消失,于是我使用前端Hack的方式取个巧,监听元素变化解决了此类问题,这里做个知识点分享

那么MutationObserver究竟是什么?如何使用?其在开发中发挥着什么作用?使用该API会有什么隐患?请继续往下看

演变过程

在Mutation标准化之前,开发者对DOM变化的非官方监听方式是使用定时器(轮询)机制,通过setTimeout或者setInterval来进行宏任务创建,观察节点的变化;

此外有些场景也可以通过事件委托机制addEventListener来监听操作及变化

后来MutationEvent的出现增强了DOM监听的拓展性和局限性,使Mutation标准化,但是MutationEvent采用的是同步的方式,并且是实时触发回调,即每次变化都会触发监听回调函数,十分损耗性能

于是就有了现在的MutationObserver,MutationObserver与Promise一样属于微任务队列,它采用的是异步的监听方式,所有的操作会统一放在回调中,当有操作时在下一个微任务执行时会触发监听回调;或者可以理解为:一个节点同时进行多个操作时,其变化会被记录到一个异步队列中,最终一次性展示,这样做既不会影响页面加载,也保证了DOM变化的监听

基础概念

MutationObserver是JS的API,可以用于观察文档中的 DOM 树变化,并在这些变化发生时执行特定的回调函数。

介绍一下基本用法,MutationObserver类接收一个回调函数,在标签发生变化时触发,参数mutationsList是MutationRecord对象(后面会详细讲)的数组,参数observer是当前MutationObserver的实例对象;observer实例存在函数observe,传入两个参数第一个是待监听的标签,第二个是配置项主要声明监听哪些属性,如childList,attributes等

const elem = document.querySelector("#elem");
// 创建观察者实例
const observer = new MutationObserver((mutationsList, observer) => {
    // 监听回调
    console.log(mutationsList, observer);
});
observer.observe(elem, {
    //至少要传一个配置
    attributes: true,
});
// 元素发生改变
elem.hidden = true;

MutationObserver

MutationObserver类的实例中有以下函数

observe(target, options)

观察指定的目标元素。第二个参数传入一个配置对象,以指定要监听的事件类型和其他选项

配置可以传入以下选项:

attributes:是否监听标签属性变化

在介绍基本用法时我们就举例说明了attributes配置,当hidden属性发生变化时,会触发监听回调

childList:是否监听子节点变化

接着上面的示例代码,我们将observe的配置变更为childList: true,就可以监听子节点的变化

<body>
  <div id="elem"></div>
  <div id="son"></div>
  <script type="text/javascript">
    const elem = document.querySelector("#elem");
    const son = document.querySelector("#son");
    const observer = new MutationObserver((mutationsList, observer) => {
      console.log(mutationsList, observer);
    });
    observer.observe(elem, {
      childList: true,
    });
    elem.textContent = "小黑";
    elem.appendChild(son);
    elem.removeChild(son);
  </script>
</body>

characterData:是否监听文本节点内容的变化

值得注意的是文本节点是标签的子节点,所以首先我们要监听标签的子节点才会有变化,比如

const elem = document.querySelector("#elem");
const textElem = elem.firstChild; // 这里获取标签的文本节点
const observer = new MutationObserver((mutationsList, observer) => {
  console.log(mutationsList, observer);
});
observer.observe(textElem, {
  characterData: true,
});
textElem.textContent = "小黑";

attributeOldValue:是否记录属性变化前的值

attributeOldValue必须配合attribute使用,我们先监听标签的attribute变化。

<body>
  <div id="elem" name="阿黄"></div>
  <script type="text/javascript">
    const elem = document.querySelector("#elem");
    const observer = new MutationObserver((mutationsList, observer) => {
      console.log(mutationsList, observer);
    });
    observer.observe(elem, {
      attributes: true,
    });
    elem.setAttribute("name", "小黑");
  </script>
</body>

当我们监听attributes属性的时候,会发现oldValue是null

如果我们加上

observer.observe(elem, {
  attributes: true,
  attributeOldValue: true,
});

就会存储原先的属性值

characterDataOldValue:是否记录文本节点内容变化前的值

与attributeOldValue类似,characterDataOldValue是用来记录储存原先的文本值的,我们将文本改成小黑,可以看到在回调中oldValue的值是之前的阿黄

const elem = document.querySelector("#elem");
const textElem = elem.firstChild;
const observer = new MutationObserver((mutationsList, observer) => {
    console.log(mutationsList, observer);
});
observer.observe(textElem, {
    characterData: true,
    characterDataOldValue: true,
});
textElem.textContent = "小黑";

subtree:是否监听后代节点变化

我们依旧以上面的代码为例,如果有两个div嵌套,并且想监听最底层的div变化,此时就可以添加属性subtree和待监听的属性,比如监听所有后代节点的属性变化

<body>
  <div id="elem">
    <div>
      <div></div>
    </div>
  </div>
  <script type="text/javascript">
    const elem = document.querySelector("#elem");
    const child = elem.firstElementChild.firstElementChild;
    const observer = new MutationObserver((mutationsList, observer) => {
      console.log(mutationsList, observer);
    });
    observer.observe(elem, {
      subtree: true,
      attributes: true,
    });
    child.setAttribute("name", "阿黄");
  </script>
</body>

attributeFilter:过滤属性名称

在配置了attributes用来监听属性变化的同时,可以使用attributeFilter配置项来过滤属性名称,attributeFilter通过传入字符串数组[ “class”,”name” ]来进行过滤,比如我只想监听class名的变化

<body>
  <div id="elem"></div>
  <script type="text/javascript">
    const elem = document.querySelector("#elem");
    const observer = new MutationObserver((mutationsList, observer) => {
      console.log(mutationsList, observer);
    });
    observer.observe(elem, {
      attributes: true,
      attributeFilter: ["class"],
    });
    elem.setAttribute("name", "阿黄");
    elem.setAttribute("class", "elem");
    elem.hidden = true;
  </script>
</body>

此时只会显示class被修改后的回调

disconnect()

当我们需要取消监听标签变化时可以使用实例化对象MutationObserver的disconnect()函数进行中断,由于Dom树变化是异步的,所以使用延时来触发取消监听

const elem = document.querySelector("#elem");
const observer = new MutationObserver((mutationsList, observer) => {
    console.log(mutationsList, observer);
});
observer.observe(elem, {
    attributes: true,
});
elem.setAttribute("name", "阿黄");
setTimeout(() => {
    observer.disconnect();
    elem.hidden = true;
});

takeRecords()

在回调函数中第一个参数是mutationsList数组,此时我们如果想清空这个数组可以使用takeRecords函数达到重置的效果

const elem = document.querySelector("#elem");
const observer = new MutationObserver((mutationsList, observer) => {
    console.log(mutationsList, observer);
});
observer.observe(elem, {
    attributes: true,
});
elem.setAttribute("name", "阿黄");
elem.hidden = true;
observer.takeRecords();
elem.setAttribute("name", "小黑");

上述代码运行后只会打印name设置为小黑的操作

MutationRecord[]

在MutationObserver类实例化时传入一个观察者回调函数,其第一个参数是一个MutationRecord数组,接收的是发生变化的元素信息

MutationRecord的属性

target:发生变化的节点

type:变化的类型

  • attributes:属性被添加、修改或删除
  • characterData:标签的文本发生变化
  • childList:子节点被添加、修改顺序或删除

nextSibling:父节点的子节点后一位兄弟节点(insertBefore,removeChild)

previousSibling:父节点的子节点前一位兄弟节点(appendChild)

attributeName:当type是attributes时,表示发生变化的属性名称(setAttribute)

attributeNamespace:当type为attributes时,表示发生变化的属性命名空间名称(setAttributeNS)

addedNodes:被添加的节点

removedNodes:被删除的节点

oldValue:当配置了attributeOldValue或characterDataOldValue为true时记录的旧值

下面这段代码几乎涵盖了上述全部属性,可以参考一下

<body>
  <div id="elem" name="阿黄">elem</div>
  <div id="son">son</div>
  <div id="prev">prev</div>
  <div id="next">next</div>
  <script type="text/javascript">
    const elem = document.querySelector("#elem");
    const son = document.querySelector("#son");
    const prev = document.querySelector("#prev");
    const next = document.querySelector("#next");
    const elemText = elem.firstChild;
    const observer = new MutationObserver((mutationsList, observer) => {
      console.log(mutationsList);
    });
    observer.observe(elem, {
      attributes: true,
      attributeOldValue: true,
      characterData: true,
      characterDataOldValue: true,
      subtree: true,
      childList: true,
    });
    // type: "characterData", oldValue: "elem"
    elemText.textContent = "阿黄";
    // oldValue: "阿黄", type: "attributes", attributeName :  "name"
    elem.setAttribute("name", "小黑");
    // attributeName: "name", attributeNamespace: "ns", type: "attributes"
    elem.setAttributeNS("ns", "NS:name", "阿黄");
    // type: "childList", removedNodes: NodeList[text], addedNodes: NodeList[text]
    elem.textContent = "小黑";
    // addedNodes:NodeList[div#prev], type: "childList"
    elem.appendChild(prev);
    // addedNodes:NodeList[div#next], type: "childList", previousSibling: div#prev
    elem.appendChild(next);
    // addedNodes: NodeList[div#son], type: "childList", previousSibling: div#prev, nextSibling: div#next
    elem.insertBefore(son, next);
    // removedNodes: NodeList[div#son], type: "childList", previousSibling: div#prev, nextSibling: div#next
    elem.removeChild(son);
  </script>
</body>

MutationObserver的应用场景

下面是一些常用的场景

检测DOM变化并做出响应

比如使用MutationObserver实现图片懒加载,监视img标签的visibilitychange事件,做出响应;或者当元素的偏移top在窗口内时做出加载图片操作

动态样式变化

监听style或者class的变化做出响应,比如我之前的应用:监听antd的模态窗变化,做出后续操作

标签之间通信

通过监听data-key属性的变化发送、接收消息

缺点

MutationObserver固然好用,但是其缺点也比较明显

首先是性能损耗

虽然在MutationEvent的基础上优化了许多,但是监听body的操作对性能影响还是非常大的,一切用户操作可能都会使函数频繁的回调。

解决方式是尽量对小范围的节点进行监听,或者限制监听类型

其次是操作冲突

由于回调函数非唯一性,如果两个观察者监听变化后的操作有依赖关系可能会造成错误或者冲突

解决方式可以采用锁的机制,当两个条件都满足才能进入函数或者线程

最后是无法在IFrame中监听变化

MutationObserver操作是基于当前DOM进行监听的,所以无法跨线程与窗口

可以使用postmessage进行通信操作,可以参考之前关于窗口与线程通信的一篇文章

总结

本篇文章介绍了MutationObserver类的基本概念及使用,监听DOM的方式由最早的定时器、事件委托到MutationEvent最后到本文介绍的MutationObserver;它采用的是异步非实时的监听方式,监听回调返回一个MutationRecord列表,记录Dom的操作变化;此外,我们可以通过实例的observe对某个节点进行监听,监听的类型主要有attributes(属性),childList(子节点变化),characterData(文本节点变化),其他配置项还有attributeOldValue(记录属性旧值),characterDataOldValue(记录文本旧值),subtree(监听后代节点),attributeFilter(属性名过滤);最后介绍了MutationObserver的应用场景及缺点,应用场景主要就是监听DOM变化采取对应操作,缺点主要是:性能损耗,操作冲突,线程限制;

以上就是文章全部内容,希望对你有帮助,如果觉得文章不错,还请三连支持一下作者,非常感谢!