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

简介: 作为前端开发,你了解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进行通信操作,可以参考之前关于窗口与线程通信的一篇文章

总结

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

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

相关文章
|
JavaScript 前端开发 数据安全/隐私保护
作为前端你还不懂MutationObserver?那Out了
[utationObserver构造函数用来监听DOM的变化,比如节点增加、删除,属性的改变,文本的变动都能监听到
295 1
作为前端你还不懂MutationObserver?那Out了
|
28天前
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
118 2
|
28天前
|
JavaScript 前端开发 程序员
前端学习笔记——node.js
前端学习笔记——node.js
36 0
|
28天前
|
人工智能 自然语言处理 运维
前端大模型应用笔记(一):两个指令反过来说大模型就理解不了啦?或许该让第三者插足啦 -通过引入中间LLM预处理用户输入以提高多任务处理能力
本文探讨了在多任务处理场景下,自然语言指令解析的困境及解决方案。通过增加一个LLM解析层,将复杂的指令拆解为多个明确的步骤,明确操作类型与对象识别,处理任务依赖关系,并将自然语言转化为具体的工具命令,从而提高指令解析的准确性和执行效率。
|
28天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
28天前
|
机器学习/深度学习 弹性计算 自然语言处理
前端大模型应用笔记(二):最新llama3.2小参数版本1B的古董机测试 - 支持128K上下文,表现优异,和移动端更配
llama3.1支持128K上下文,6万字+输入,适用于多种场景。模型能力超出预期,但处理中文时需加中英翻译。测试显示,其英文支持较好,中文则需改进。llama3.2 1B参数量小,适合移动端和资源受限环境,可在阿里云2vCPU和4G ECS上运行。
|
28天前
|
前端开发 算法 测试技术
前端大模型应用笔记(五):大模型基础能力大比拼-计数篇-通义千文 vs 文心一言 vs 智谱 vs 讯飞vsGPT
本文对比测试了通义千文、文心一言、智谱和讯飞等多个国产大模型在处理基础计数问题上的表现,特别是通过链式推理(COT)提示的效果。结果显示,GPTo1-mini、文心一言3.5和讯飞4.0Ultra在首轮测试中表现优秀,而其他模型在COT提示后也能显著提升正确率,唯有讯飞4.0-Lite表现不佳。测试强调了COT在提升模型逻辑推理能力中的重要性,并指出免费版本中智谱GLM较为可靠。
前端大模型应用笔记(五):大模型基础能力大比拼-计数篇-通义千文 vs 文心一言 vs 智谱 vs 讯飞vsGPT
|
2月前
|
SpringCloudAlibaba JavaScript 前端开发
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
分布式组件、nacos注册配置中心、openfegin远程调用、网关gateway、ES6脚本语言规范、vue、elementUI
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
|
3月前
|
存储 前端开发 JavaScript
前端语言串讲 | 青训营笔记
前端语言串讲 | 青训营笔记
40 0
|
5月前
|
JSON 前端开发 JavaScript
前端Ajax、Axios和Fetch的用法和区别笔记
前端Ajax、Axios和Fetch的用法和区别笔记
91 2