DOM 规范 — MutationObserver 接口

前言

最近在重学 JavaScript 中,再一次接触到了 MutationObserver 内容,接着联想到了 Vue 源码中有使用过这个接口,因此觉得有必要对 MutationObserver 接口进行相关了解和学习。

下面是 vue 源码中关于 MutationObserver 接口使用的代码:

MutationObserver

主要作用

MutationObserver 可以观察整个 文档DOM 树的一部分具体 dom 元素,主要是观察元素的 属性、子节点、文本 的变化,并且可以在 DOM 被修改时异步执行回调。

MutationObserver 接口是为了取代废弃的 MutationEvent:

  • DOM Level 2 规范中描述的 MutationEvent 定义了一组会在各种 DOM 变化时触发的事件。由于浏览器事件的实现机制,这个接口出现了严重的性能问题。因此,DOM Level 3 规定废弃了这些事件。
  • MutationObserver 接口更实用、性能更好

基本用法

MutationObserver 的实例要通过调用 MutationObserver 构造函数并传入一个回调函数来创建,这个回调函数会接收两个参数:

  • mutationRecord —— 是一个数组存储的是 MutationRecord 的实例,数组的每一项包含发生了什么变化,以及 DOM 的哪一部分受到了影响。因为回调执行之前可能同时发生多个满足被观察 dom 修改的条件,所以当前回调就会被执行多次,每次执行回调都会传入一个包含按顺序入队的 MutationRecord 实例的数组;
  • mutationObserver —— 是观察变化的 MutationObserver 实例,也就是外部实例化得到的 observer 对象;
 let observer = new MutationObserver((mutationRecord, mutationObserver) => {console.log('DOM was mutated!');});console.log("observer = ", observer);

image.png
并且得到的 observe 实例可以调用 MutationObserver 原型上的三个方法:

  • observe()
  • disconnect()
  • takeRecords()

MutationObserverInit 对象

在正式介绍以上三个方法之前,有必要先了解一下 MutationObserverInit 对象,因为 observe() 方法的第二个参数需要接收的就是一个 MutationObserverInit 对象。

MutationObserverInit 对象用于控制对目标节点的观察范围,简单点说,就是 observe 实例可以检测的事件内容包括:

  • 属性变化 —— 如:dom.removeAttribute() || dom.setAttribute() 等
  • 文本变化 —— 如: dom.innerText = xxx || dom.innerHTML = xxx || dom.textContent = xxx 等
  • 子节点变化 —— 如:dom.appendChild() || dom.insertBefore() || dom.replaceChild() || dom.removeChild() 等

MutationObserverInit 对象的属性,它们的值除了 attributeFilter 属性值为数组之外,全为 Boolean 类型:

  • subtree —— true 表示需要检测子节点的变化,false 则相反
  • attributes —— true 表示需要检测属性变化,false 则相反
  • attributeFilter —— 字符串数组,表示要观察哪些属性的变化
  • attributeOldValue —— true 表示 MutationRecord 需要记录变化之前的属性值,false 则相反,一旦这个属性设置为 true ,会把 attributes 的值也设置为 true
  • characterData —— true 表示修改文本内容是否触发变化事件,false 则相反
  • characterDataOldValue —— true 表示 MutationRecord 需要记录变化之前的字符数据,false 则相反,一旦这个属性设置为 true ,会把 characterData 的值也设置为 true
  • childList —— 表示修改目标节点的子节点是否触发变化事件,false 则相反
    总结,就是一个对象拥有符合 MutationObserverInit 上定义的这些属性,就能被称为 MutationObserverInit 对象

在调用 observe() 时,MutationObserverInit 对象中的 【attribute、characterData 、childList】或 【attributeOldValue、characterDataOldValue】 必须至少有一项为 true。否则会抛出错误,因为没有任何变化事件能触发回调,但是又注册了回调。

observe() 方法

关联 observer 和 dom

新创建的 MutationObserver 实例不会关联 DOM 的任何部分,必须要通过 observer.observe() 方法,把 observerDOM 进行关联。

observer.observe(dom, mutationObserverInit) 中两个必需参数:

  • dom —— 要观察其变化的 DOM 节点
  • mutationObserverInit —— 符合 MutationObserverInit 定义的对象
    下面的例子就是观察 <body> 标签上的属性变化:
// 实例化 observer 并注册回调let observer = new MutationObserver((mutationRecord, mutationObserver) =>{// 大约 2s 执行这个回调 console.log('body attributes changed!!!'); // body attributes changed!!!console.log('mutationRecord = ', mutationRecord); // [MutationRecord]console.log('mutationObserver === observer', mutationObserver === observer);// true});// 将 observer 实例与目标 dom 进行关联observer.observe(document.body, { attributes: true });// 大约 2s 后修改 body 标签的 class 值setTimeout(() => {document.body.setAttribute('class', 'body')}, 2000)

回调函数中的 MutationRecord 实例

上面 console.log('mutationRecord = ', mutationRecord) 的输出结果如下:

 mutationRecord =  [{addedNodes: NodeList [],attributeName: "class",attributeNamespace: null,nextSibling: null,oldValue: null,previousSibling: nullremovedNodes: NodeList [],target: body.bodytype: "attributes"}]

下面是每个属性对应的解释:

  • target —— 被修改影响的目标 dom 节点
  • type —— 表示变化的类型,也就是 MutationObserverInit 对象中的三种:“attributes”、“characterData” 或 “childList”
  • attributeName —— 针对 “attributes” 类型的变化时,保存被修改属性的名字
  • attributeNamespace —— 对于使用了命名空间的 “attributes” 类型的变化,保存被修改属性的名字,其他变化事件会将这个属性设置为 null
  • oldValue —— 如果在 MutationObserverInit 对象中启用(attributeOldValue 或 characterData OldValue 为 true),则 “attributes” 或 “characterData” 的变化事件会设置这个属性为被替代的值;“childList” 类型的变化始终将这个属性设置为 null
  • addedNodes —— 针对 “childList” 类型的变化,返回包含变化中添加节点的 NodeList,其他变化事件会将这个属性设置为空 NodeList 数组
  • previousSibling —— 对于 “childList” 类型的变化,返回包含变化中删除节点的 NodeList,默认为空 NodeList
  • nextSibling —— 对于 “childList” 类型的变化,返回变化节点的后一个同胞 Node,默认为 null
  • removedNodes —— 对于"childList"类型的变化,返回变化节点的前一个同胞 Node,默认为 null

disconnect() 方法

默认情况下,只要被观察的元素不被垃圾回收,MutationObserver 的回调就会响应 DOM 变化事 件,从而被执行。要提前终止执行回调,可以调用 disconnect() 方法。

直接看下面的例子:

            let observer = new MutationObserver((mutationRecord, mutationObserver) => {console.log(mutationRecord);console.log(mutationObserver);})observer.observe(document.body, { attributes: true });// 位置1observer.disconnect();setTimeout(() => {// 位置2// observer.disconnect();document.body.setAttribute('class', 'body');// 位置3// observer.disconnect();}, 2000);// 位置4// observer.disconnect();

上面我们把 observer.disconnect() 分别放在 位置 1、2、3、4,但实际上它们的效果都是一样的,都会直接终止执行回调,要想让已经加入任务队列的回调执行,可以利用事件循环机制,比如:区分同步和异步修改,然后在异步操作中调用 disconnect() ,保证让已经入列的回调执行完毕。

takeRecords() 方法

调用 MutationObserver 实例的 takeRecords() 方法可以清空记录队列,取出并返回包含其中的所有 MutationRecord 实例的数组。

使用场景: 希望断开与观察目标的联系,但又希望获取调用 disconnect() 而被抛弃的记录队列中的 MutationRecord,这样即使已经断开关联,也能继续处理后续操作。

     // 1. 未调用 takeRecords()let observer = new MutationObserver((mutationRecord, mutationObserver) => {console.log('body had mutated!!!')console.log(mutationRecord); // [MutationRecord, MutationRecord, MutationRecord]},)observer.observe(document.body, { attributes: true })document.body.className = 'body1'document.body.className = 'body2'document.body.className = 'body3'// 2. 调用 takeRecords()let observer = new MutationObserver(// 这个回调函数不再执行,因为已经通过 observer.takeRecords 获取到了 mutationRecord(mutationRecord, mutationObserver) => {console.log('body had mutated!!!')console.log(mutationRecord); // [MutationRecord, MutationRecord, MutationRecord]},)observer.observe(document.body, { attributes: true })document.body.className = 'body1'document.body.className = 'body2'document.body.className = 'body3'console.log(observer.takeRecords()); // 这里输出 [MutationRecord, MutationRecord, MutationRecord]console.log(observer.takeRecords());  // 上面获取到集合之后,再次获取,此时已经被清空,输出: [] 

复用 MutationObserver 对象

多次调用 observe() 方法,可以复用一个 MutationObserver 对象观察多个不同的目标节点。此时,MutationRecordtarget 属性可以标识发生变化事件的目标节点。

      let h1 = document.createElement('h1')let h2 = document.createElement('h2')let observer = new MutationObserver((mutationRecord, mutationObserver) => {console.log(mutationRecord)},)// 初次检测observer.observe(h1, {attributes: true,})// 再次检测observer.observe(h2, {attributes: true,})h1.className = 'h1'h1.textContent = 'this is h1'h2.className = 'h2'h2.textContent = 'this is h2'// 即使没有把 h1 和 h2 节点添加的文档中,上面的对 className 的修改,也可以触发回调执行document.body.appendChild(h1)document.body.appendChild(h2)

observer.disconnect() 方法调用之后,所有和 observer 关联的 dom 就全部断开,但是后续可以继续使用 observer.observe() 方法重新关联。

MutationObserver 回调与记录队列

MutationObserver 接口是出于性能考虑而设计的,其核心是异步回调与记录队列模型。为了在大量变化事件发生时不影响性能,每次变化的信息(由 oberver 实例决定)会保存在 MutationRecord 实例中,然后添加到记录队列。

记录队列对每个 MutationObserver 实例都是唯一的,是所有 DOM 变化事件的有序列表。

根据下面的例子来简单理解,下面的 body 元素虽然被连续修改 2 次,但是我们注册的回调函数不会被执行 2 次,而是把 2 次操作的信息分别放到 MutationRecord 的实例中,并通过数组进行保存,这样就保证了多次修改的内容都能在一次回调执行中获取到。

// 实例化 observer 对象并注册回调
let observer = new MutationObserver((mutationRecord, mutationObserver) => {console.log(mutationRecord);// 这里输出的是两次修改的集合
})
// 将 observer 与 dom 进行关联
observer.observe(document.body, { attributes: true });
// 连续两次修改属性值
document.body.className = "body1";
document.body.className = "body2";

使用 MutationObserver 仍然是有代价

虽然在上面说了不少 MutationObserver 的优势,但是应该要理解为是与旧的MutationEvent 相比的情况下,因为 MutationObserver 本身还是存在缺点的。
这也就是为什么 vue 源码中没有直接使用它的原因,当然在 vue 中它是仅次于 promise 的,因为 MutationObserver 和 Promise 一样属于微任务,能够被事件循环尽快执行。

  • MutationObserver 的引用

    • MutationObserver 对要观察的目标节点的引用属于弱引用,所以不会妨碍垃圾回收程序回收目标节点
    • 目标节点对 MutationObserver 的引用属于强引用。如果目标节点从 DOM 中被移除,随后被垃圾回收,则关联的 MutationObserver 也会被垃圾回收。
  • MutationRecord 的引用

    • 记录队列中的每个 MutationRecord 实例至少包含对已有 DOM 节点的一个引用,即里面的 target 属性,如果变化是 childList 类型,则会包含多个节点的引用
    • 记录队列和回调处理的默认行为是耗尽这个队列,处理每个 MutationRecord,然后让它们超出作用域并被垃圾回收
    • 有时候可能需要保存某个观察者的完整变化记录,那么就保存所有的 MutationRecord 实例,也就会保存它们引用的节点,而这会妨碍这些节点被回收
    • 如果需要尽快地释放内存,可以从每个 MutationRecord 中抽取出最有用的信息,保存到一个新对象,然后释放 MutationRecord 中的引用

最后

既然开头提到了 vue2 源码中对 MutationObserver 的使用,其实也就是和 nextTick 源码相关的部分,那么在这就简单的总结一下:

  • 先定义了一个 callbacks 存放所有的 nextTick 里的回调函数
  • 然后判断当前环境是否支持 Promise,如果支持,就用 Promise 来触发回调函数
  • 如果不支持 Promise 就判断是否支持 MutationObserver,通过观察文本节点发生变化,去触发执行所有异步回调函数
  • 如果不支持 MutationObserver 就判断是否支持 setImmediate,如果支持,就通过setImmediate 来触发回调函数
  • 如果以上都不支持就只能用 setTimeout 来完成异步执行

延迟调用优先级如下:

Promise > MutationObserver > setImmediate > setTimeout

如果想了解 事件循环机制Promise 的内容可以参考之前的文章:

  • JavaScript 事件循环(EventLoop) —— 浏览器 & Node

  • 带你手写 Promise,别再死记硬背了!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/470567.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

灰狼优化算法

一、简介 1.1 灰狼优化算法-Grey Wolf Optimizer 通过模拟灰狼群体捕食行为&#xff0c;基于狼群群体协 作的机制来达到优化的目的。&#xff27;&#xff37;&#xff2f;算法具有结构简单、需 要调节的参数少、容易实现等特点&#xff0c;其中存在能够自适应调整 的收敛因子…

AI 写作(五)核心技术之文本摘要:分类与应用(5/10)

一、文本摘要&#xff1a;AI 写作的关键技术 文本摘要在 AI 写作中扮演着至关重要的角色。在当今信息爆炸的时代&#xff0c;人们每天都被大量的文本信息所包围&#xff0c;如何快速有效地获取关键信息成为了一个迫切的需求。文本摘要技术正是为了解决这个问题而诞生的&#x…

【 ElementUI 组件Steps 步骤条使用新手详细教程】

本文介绍如何使用 ElementUI 组件库中的步骤条组件完成分步表单设计。 效果图&#xff1a; 基础用法​ 简单的步骤条。 设置 active 属性&#xff0c;接受一个 Number&#xff0c;表明步骤的 index&#xff0c;从 0 开始。 需要定宽的步骤条时&#xff0c;设置 space 属性即…

尽量通俗易懂地概述.Net U nity跨语言/跨平台相关知识

本文参考来自唐老狮,Unity3D高级编程:主程手记,ai等途径 仅作学习笔记交流分享 目录 1. .Net是什么? 2. .Net框架的核心要点? 跨语言和跨平台 .Net x Unity跨平台发展史 Net Framework 2002 Unity跨平台之 Mono 2004 Unity跨平台之 IL2CPP 2015 二者区别 .NET Core …

基于yolov8、yolov5的番茄成熟度检测识别系统(含UI界面、训练好的模型、Python代码、数据集)

摘要&#xff1a;番茄成熟度检测在农业生产及质量控制中起着至关重要的作用&#xff0c;不仅能帮助农民及时采摘成熟的番茄&#xff0c;还为自动化农业监测提供了可靠的数据支撑。本文介绍了一款基于YOLOv8、YOLOv5等深度学习框架的番茄成熟度检测模型&#xff0c;该模型使用了…

应用程序部署(IIS的相关使用,sql server的相关使用)

数据服务程序&#xff08;API&#xff09;部署 1、修改配置文件 打开部署包中的web.config配置文件&#xff0c;确认数据库登录名和密码正确 修改ip为电脑IP&#xff08;winR输入cmd&#xff0c;输入ipconfig&#xff0c;IPv4对应的就是本机IP&#xff09; 2、打开IIS&#x…

网页版五子棋——对战模块(服务器端开发②)

前一篇文章&#xff1a;网页版五子棋——对战模块&#xff08;服务器端开发①&#xff09;-CSDN博客 项目源代码&#xff1a;Java: 利用Java解题与实现部分功能及小项目的代码集合 - Gitee.com 目录 前言 一、创建并注册 GameAPI 类 1.创建 GameAPI 类 2.注册 GameAPI 类 …

STM32单片机WIFI语音识别智能衣柜除湿消毒照明

实践制作DIY- GC0196-WIFI语音识别智能衣柜 一、功能说明&#xff1a; 基于STM32单片机设计-WIFI语音识别智能衣柜 二、功能介绍&#xff1a; STM32F103C系列最小系统板LCD1602显示器ULN2003控制的步进电机&#xff08;柜门开关&#xff09;5V加热片直流风扇紫外消毒灯DHT11…

网络远程操控

1.给两个设备配上ip地址让他们能通 2.开启远程管理功能&#xff0c;打开telnet 3.创建远程管理的账号和密码&#xff0c;账号权限 输入system-view进入视图&#xff0c;不敲这个命令不能进行配置 配好ip后进入AR1ping一下AR2的ip看看通不通&#xff0c;接着进入AR2开启telnet权…

【go从零单排】Timer、Epoch 时间函数

&#x1f308;Don’t worry , just coding! 内耗与overthinking只会削弱你的精力&#xff0c;虚度你的光阴&#xff0c;每天迈出一小步&#xff0c;回头时发现已经走了很远。 &#x1f4d7;概念 在 Go 语言中&#xff0c;time.Timer 是一个用于在指定时间后执行操作的计时器。…

鸿蒙自定义UI组件导出使用

上期讲解了在Entry入口写了一个系统的下拉列表组件&#xff0c;如果我们想要封装一个可供复用的组件供团队其他人使用&#xff0c;那么需要掌握一下自定义组件的写法&#xff1a; 1、自定义可导入组件 - export 声明模块 如果要定义一个在外部可使用的组件 , 需要再定义组件…

Web大学生网页作业成品——婚礼婚纱网页设计与实现(HTML+CSS)(6个页面)

&#x1f389;&#x1f389;&#x1f389; 常见网页设计作业题材有**汽车、环保、明星、文化、国家、抗疫、景点、人物、体育、植物、公益、图书、节日、游戏、商城、旅游、家乡、学校、电影、动漫、非遗、动物、个人、企业、美食、婚纱、其他**等网页设计题目, 可满足大学生网…

时序数据库TimescaleDB安装部署以及常见使用

文章目录 一、时序数据库二、TimescaleDB部署1、repository yum仓库配置2、yum在线安装3、插件配置4、TimescaleDB使用登录pg创建插件使用超表 一、时序数据库 什么是时序数据库&#xff1f;顾名思义&#xff0c;用于处理按照时间变化顺序的数据的数据库即为时序数据库&#x…

Matlab: 生成对抗网络,使用Datastore结构输入mat格式数据

使用matlab的生成对抗网络&#xff08;Generative Adversarial Network&#xff0c;GAN&#xff09;以及条件CGAN时&#xff0c;案例中 的生成器的输入为图像&#xff0c;改为.mat格式输入遇到的问题。解决方法 官方资源 训练条件生成对抗网络 (CGAN)- MATLAB & Simulink-…

Linux kernel 堆溢出利用方法(二)

前言 本文我们通过我们的老朋友heap_bof来讲解Linux kernel中off-by-null的利用手法。在通过讲解另一道相对来说比较困难的kernel off-by-null docker escape来深入了解这种漏洞的利用手法。&#xff08;没了解过docker逃逸的朋友也可以看懂&#xff0c;毕竟有了root权限后&a…

设计模式:工厂方法模式和策略模式

工厂方法模式 什么是开闭原则? 开闭原则是扩展开发,对修改关闭 简单工厂(不是设计模式而是一种编程的习惯) 有三个角色 抽象产品:定义了产品的规范,描述了产品的特性和功能.具体产品:实现或者继承抽象产品的子类具体工厂:提供了创建产品的方法,调用者通过该方法获取产品 实…

深度学习代码笔记

一、U-NET 论文题目&#xff1a;U-Net: Convolutional Networks for Biomedical Image SegmentationUNet 的体系结构基于编码器-解码器范式&#xff0c;其中编码器从输入图像中提取特征&#xff0c;解码器基于这些特征生成分割图。但是&#xff0c;UNet还集成了编码器和解码器…

软件测试面试2024最新热点问题

大厂面试热点问题 1、测试人员需要何时参加需求分析&#xff1f; 如果条件循序 原则上来说 是越早介入需求分析越好 因为测试人员对需求理解越深刻 对测试工作的开展越有利 可以尽早的确定测试思路 减少与开发人员的交互 减少对需求理解上的偏差 2、软件测试与调试的关系 测…

L10.【LeetCode笔记】回文链表

目录 1.题目 2.自解 代码 提交结果 1.题目 给你一个单链表的头节点 head &#xff0c;请你判断该链表是否为 回文链表 。如果是&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,2,1] 输出&#xff1a;tru…

Lucene 和 Elasticsearch 中更好的二进制量化 (BBQ)

作者&#xff1a;来自 Elastic Benjamin Trent Lucene 和 Elasticsearch 中更好的二进制量化 (BBQ)。 嵌入模型输出 float32 向量&#xff0c;通常对于高效处理和实际应用来说太大。Elasticsearch 支持 int8 标量量化&#xff0c;以减小向量大小&#xff0c;同时保持性能。其他…