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

「前言」

「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 进行通信操作,可以参考之前关于窗口与线程通信[1]的一篇文章」

「总结」

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

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

原创文章,作者:guozi,如若转载,请注明出处:https://www.sudun.com/ask/79543.html

Like (0)
guozi的头像guozi
Previous 2024年5月30日 下午6:55
Next 2024年5月30日

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注