Dom-api MutationObserver使用方法详解

目录
  • 1. 概述
  • 2. 基本使用
    • 2.1 observer 方法
    • 2.2 MutationObserverInit 对象
    • 2.3 disconnect()方法
    • 2.4 takeRecords
  • 3. MutationRecord
    • 3.1 MutationRecord 实例
  • 4. MutationObserver 实战
  • 总结

1. 概述

MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。 - MDN

也就是说,当监视的 DOM 发生变动时 MutationObserver 将收到通知并触发事先设定好的回调函数。这个功能非常强大,意味着对于我们可以更加方便的动态操作 DOM 元素了。

你是否能联想到某些业务场景呢?

像这样的列表页,由于文案和文章配图数量的不同导致有多种不同的 ui 设计和排列方式,所以在前端对数据渲染的时候,要对列表每一项内容类型进行甄别。使用 MutationObserver 可以非常简单的完成这个需求

2. 基本使用

MutationObserver 是一个构造函数,通过调用 MutationObserver 构造函数并传入一个回调函数来创建一个观察 DOM 的实例

const observer = new MutationObserver(() => console.log('DOM 发生变化了~'));

回调参数的两个参数:

  • mutationRecords:数组队列,记录操作的结果
  • observer:与构造函数的返回值 全等,因为这个回调函数是 异步执行,所以也可以访问到外部的 observer

后文还会再详细讨论这两个参数

2.1 observer 方法

新创建的 MutationObserver 实例不会关联 DOM 的任何部分。要把这个 observerDOM 关联起来,需要使用 observe()方法

observer.observe(document.body, { attributes: true });

这个方法接收必需的参数:

  • 第一个参数:要观察的 DOM 节点
  • 第二个参数:MutationObserverInit 对象

这样 document.body 就被观察了,只要 document.body 元素的任何属性值发生变化,就会触发观察对象,并且 异步调用 传入 MutationObserver 的回调函数(这是一个 微任务

const observer = new MutationObserver(() => console.log('DOM 发生变化了~'));
observer.observe(document.body, { attributes: true });
setTimeout(() => {
  document.body.className = 'test';
}, 1000);

等过了一秒之后,定时器的回调函数执行,修改了 document.bodyclass 属性,所以了触发 MutationObserver 的回调函数

2.2 MutationObserverInit 对象

在上面的例子中,只要 document.body 本身的任意属性发生了,都会被观察到,但是其他修改 DOM 的行为不会被观察,例如节点的增删改查,子节点属性的修改...,因为我们在调用 observe() 方法的时候传入的 MutationObserverInit 对象添加了 attributes 属性,所以 observe() 方法作用是只能侦测自身的元素属性值的变化。MutationObserverInit 对象除了这个属性之外,还有很多非常强大的属性可以观察更多的节点操作

MutationObserverInit 对象用于控制对目标节点的观察范围。观察方式的类型有 属性变化文本变化子节点变化 这三种。

所以在调用 observe()时,MutationObserverInit 对象中的 attribute属性变化)、characterData文本变化) 和 childList子节点变化) 属性必须 至少有一项true(无论是直接设置这几个属性,还是通过设置 attributeOldValue属性变化)等属性间接导致它们的值转换为 true)。否则会抛出错误,因为 DOM 的变化不会被任何变化事件类型触发回调。

  • 属性变化

观察节点 属性添加移除修改。需要在 MutationObserverInit 对象中将 attributes 属性设置为 true

const observer = new MutationObserver(() => console.log('DOM 发生变化了~'));
observer.observe(document.body, { attributes: true });
setTimeout(() => {
  document.body.className = 'test';
}, 1000);

还有 attributeOldValue: true:可以记录变化之前的属性值。
attributeFilter: ['class', 'id']:可以观察哪些属性的变化,在这里只观察了 classid 属性

  • 文本变化

观察文本节点(如 Text 文本节点Comment 注释 ) 中字符的 添加删除修改。要在 MutationObserverInit 对象中将 characterData 属性设置为 true

const observer = new MutationObserver(() => console.log('DOM 发生变化了~'));
observer.observe(document.body.firstChild, { characterData: true });
setTimeout(() => {
  document.body.firstChild.textContent = '123';
}, 1000);

还有 characterDataOldValue:可以记录变化之前的文本值

  • 观察子节点

观察目标节点子节点的添加和移除。需要在 MutationObserverInit 对象中将 childList 属性设置为 true

const observer = new MutationObserver(() => console.log('DOM 发生变化了~'));
observer.observe(document.body, { childList: true });
setTimeout(() => {
  document.body.appendChild(document.createElement('div'));
}, 1000);

在这个例子中控制台输出两次,第一次是 body 元素在 0s 触发回调,第二次才是新创建的元素在 1s 之后触发回调,因为观察 document.body 会在创建 body 的时候就立即被观察到,而观察非 body 元素,不会触发自身创建的过程

childList 只会观察子节点,但不会观察深层的节点,可以在 MutationObserverInit 对象中将 subtree 属性设置为 true,还得将 childListtrue,因为 MutationObserverInit 对象中的 attributecharacterDatachildList 属性必须 至少有一项true

<div></div>
<script>
  const observer = new MutationObserver(mutationRecords => {
    console.log('触发了');
    console.log(mutationRecords.length); // 2
  });
  observer.observe(document.body.children[0], { childList: true, subtree: true });
  setTimeout(() => {
    document.body.children[0].appendChild(document.createElement('div'));
    document.body.children[0].children[0].appendChild(document.createElement('div'));
  }, 1000);
</script>

这里虽然只会触发一次回调,但是会在 mutationRecords 这个数组中会分别记下两次 DOM 操作的记录,所以数组的长度为 2

<div></div>
<script>
  const observer = new MutationObserver(mutationRecords => {
    console.log('触发了');
    console.log(mutationRecords.length); // 1
  });
  observer.observe(document.body.children[0], { childList: true, subtree: true });
  setTimeout(() => {
    document.body.children[0].appendChild(document.createElement('div'));
  }, 1000);
  setTimeout(() => {
    document.body.children[0].children[0].appendChild(document.createElement('div'));
  }, 1000);
</script>

这个例子与上个例子区别是将两次 DOM 操作放在两个不同的定时器执行,但是结果却是截然不同,这里会输出两次,mutationRecords 数组的长度为 1

这是因为 DOM 操作是同步的,DOM 渲染是异步的,MutationObserver 中的回调函数执行会被包裹在一个 微任务 中,而定时器是 宏任务,所以整个执行过程是:第一个定时器先执行,观察 DOM 的回调函数执行,第二个定时器再执行,所以 DOM 变化被观察了两次。

上一个的例子 DOM 操作是在同一个 宏任务 中执行,因为浏览器会优化 DOM 渲染的过程,所以等到两个 div 元素创建完毕才会渲染,之后执行观察 DOM微任务,所以才会触发一次观察,但是产生了两个结果,所以 mutationRecords 数组的长度为 2

这里还有一个怪异现象,在第二个例子中,为什么两输出 mutationRecords 的长度都是 1,因为这两个数组不是同一个数组,关于为什么 mutationRecords 数组 不会缓存 第一次的操作结果,而是创建两个不同的数组,会在后面的内容详细讨论。

2.3 disconnect()方法

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

<div></div>
<script>
  const observer = new MutationObserver(mutationRecords => {
    console.log('触发了');
  });
  observer.observe(document.body.children[0], { childList: true, subtree: true });
  setTimeout(() => {
    document.body.children[0].appendChild(document.createElement('div'));
    setTimeout(() => {
      observer.disconnect();
    }, 0);
  }, 1000);
  setTimeout(() => {
    document.body.children[0].appendChild(document.createElement('div'));
  }, 2000);
</script>

在这个例子中,在第一秒的时候执行了 DOM 操作,并且创建一个定时器包裹 disconnect() 方法,然后执行 disconnect() 方法,在第二秒的时候执行了另外一个 DOM 操作。所以结果只有第一次 DOM 操作会被观察到

为什么这里需要将 disconnect 方法计时器里执行呢,千万别忘了,DOM 操作是 同步执行 的,DOM 渲染是 异步执行 的,disconnect() 也是 同步执行 的。如果不添加定时器,在 DOM 渲染值之前就取消了观察,虽然操作了 DOM,但是渲染过程并没有观察到

2.4 takeRecords

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

const observer = new MutationObserver(mutationRecords => {
  console.log(mutationRecords); // 不输出
});
observer.observe(document.body, { attributes: true });
document.body.className = 'test1';
document.body.className = 'test2';
document.body.className = 'test3';
console.log(observer.takeRecords().length); // 3
console.log(observer.takeRecords().length); // 0

在这个例子中,操作了 3DOM,所以在调用第一次 takeRecords() 方法的时候会输出 3,并且切断了与观察对象的联系,所以不会触发 MutationObserver 的回调,但是这种切断关系是 不牢靠 的,也就意味着下次的 DOM 操作会 重启观察,就像下面的这个例子表现的一样

const observer = new MutationObserver(mutationRecords => {
  console.log(mutationRecords); // 输出两次
});
observer.observe(document.body.children[0], { attributes: true });
document.body.children[0].className = 'test1';
document.body.children[0].className = 'test2';
document.body.children[0].className = 'test3';
observer.takeRecords();
document.body.children[0].className = 'test4';
setTimeout(() => {
  document.body.children[0].className = 'test5';
});

3. MutationRecord

MutationRecord 是一个 记录队列 的数组,,仅当 微任务队列 没有其他的微任务回调时(队列中微任务 长度为 0),才会将观察者注册的 回调 作为微任务放置到任务队列上。这样可以保证记录队列的内容不会被回调处理两次。

在回调的微任务异步执行期间,有可能又会发生更多变化事件。因此被调用的回调会接收到一个 MutationRecord 实例的数组,顺序为它们进入记录队列的顺序。回调要负责处理这个数组的每一个实例,因为 回调函数 退出之后这些实现就不存在了。回调函数执行完成后,这些 MutationRecord 就用不着了, 因此记录队列会被清空,其内容会被丢弃。所以每一个回调函数中的 MutationRecords 数组是 不同的实例

3.1 MutationRecord 实例

const observer = new MutationObserver(mutationRecords => {
  console.log(mutationRecords);
});
const oDiv = document.getElementsByTagName('div')[0];
observer.observe(oDiv, { attributeOldValue: true });
oDiv.classList.add('box');

几个重要的属性:

属性 说明
target 被修改影响的目标节点
type 表示变化的类型:"attributes"、"characterData"或"childList"
oldValue 如果在 MutationObserverInit 对象中启用(attributeOldValue 或 characterData OldValue 为 true),"attributes"或"characterData&quot;的变化事件会设置这个属性为被替代的值 "childList"类型的变化始终将这个属性设置为 null
addedNodes 对于"childList"类型的变化,返回包含变化中添加节点的 NodeList 默认为空 NodeList

4. MutationObserver 实战

一个简单的业务场景:

用户提交评论,如果评论的内容超过最大宽度,需要隐藏多余的部分,同时展示“查看更多”按钮,点击这个按钮就会展示评论的全部内容

难点:只有当 DOM 被渲染的时候 才知道实际的高度,所以无法预先分析评论文本内容而选择渲染方式的类型

实现思路:使用 MutationObserver 监听评论区列表,每当用户提交新的评论,新生成的 DOM 就会被观察到,判断评论的内容是否超出最大高度,更新 UI

<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
interface ICommentItem {
  id: string;
  text: string;
  showBtn: boolean;
}
const comIptVal = ref('');
const commentList = reactive<ICommentItem[]>([]);
const commentListRef = ref<HTMLElement | null>(null);
const MaxSize = 50; // 每一项最大高度
const observer = new MutationObserver(mutationRecord => {
  const currRecord = mutationRecord[mutationRecord.length - 1]; // 最新的记录
  const newNode = currRecord.addedNodes[currRecord.addedNodes.length - 1] as HTMLElement; // 新添加的节点
  // 新增加的按钮也会触发观察,所以要判断新增加节点是否是评论
  if (newNode.className === 'comment-item') {
    const id = newNode.dataset.id;
    const item = commentList.find(item => item.id === id)!;
    if (newNode.clientHeight > MaxSize) {
      // 如果超出最大高度
      const oText = newNode.children[0] as HTMLElement;
      oText.style.height = MaxSize + 'px';
      oText.style.overflow = 'hidden';
      item.showBtn = true;
    }
  }
});
onMounted(() => {
  observer.observe(commentListRef.value as HTMLElement, {
    subtree: true,
    childList: true,
  });
});
const addCommentItem = () => {
  commentList.push({
    id: String(new Date().getTime()), // 评论的 id
    text: parseComment(comIptVal.value), // 解析输入文本内容
    showBtn: false, // 默认不超出最大高度
  });
};
const parseComment = (str: string) => {
  return str.replace(/[\n\r]/g, '<br />'); // 将 \n 换行解析成 <br /> 元素
};
const showAllBtnClick = (el: HTMLElement, item: ICommentItem) => {
  el.style.overflow = 'visible';
  el.style.height = 'auto';
  item.showBtn = false; // 隐藏点击更多按钮
};
const child = reactive<HTMLElement[]>([]); // 循环绑定 DOM
</script>
<template>
  <textarea v-model="comIptVal"></textarea>
  <button @click="addCommentItem">添加</button>
  <ul class="comment-list" ref="commentListRef">
    <li class="comment-item" v-for="(item, index) in commentList" :key="item.id" :data-id="item.id">
      <div v-html="item.text" :ref="(el: any) => child[index] = el"></div>
      <button v-if="item.showBtn" @click="showAllBtnClick(child[index] as HTMLElement, item)">
        更多
      </button>
    </li>
  </ul>
</template>

参考文献

JavaScript 高级程序设计第 4 版.PDF – 1024.Cool

总结

MutationObserver api 使用大多数场景为:动态监听 DOM 元素的变化,在传入构造函数的回调函数中可以访问到触发 DOM 变化的 target 和 影响 DOM 变化的结果

以上就是Dom-api MutationObserver使用方法详解的详细内容,更多关于Dom-api MutationObserver方法的资料请关注我们其它相关文章!

(0)

相关推荐

  • 原生JS实现几个常用DOM操作API实例

    原生实现jQuery的sibling方法 <body> <span>我是span标签</span> <div>我是一个div</div> <h1 id="h1">我是标题</h1> <p>我是一个段落</p> <script type="text/javascript"> //获取元素的兄弟节点 function siblings(o){//参数o

  • JavaScript中MutationObServer监听DOM元素详情

    一.基本使用 可以通过MutationObserver构造函数实例化,参数是一个回调函数. let observer = new MutationObserver(() => console.log("change")); console.log(observer); observer对象原型链如下: MutationObserver实例: 可以看到有disconnect.observer.takeRecords方法. 1. observer方法监听 observer方法用于关联

  • 基于JavaScript操作DOM常用的API小结

    前言 DOM(Document Object Model)即文档对象模型,针对 HTML 和 XML 文档的 API(应用程序接口).DOM 描绘了一个层次化的节点树,运行开发人员添加.移除和修改页面的某一部分.DOM 脱胎于 Netscape 及微软公司创始的 DHTML(动态 HTML),但现在它已经成为表现和操作页面标记的真正跨平台.语言中立的方式. 阅读目录 基本概念 节点创建型api 页面修改型API 节点查询型API 节点关系型api 元素属性型api 元素样式型api 总结 文本整

  • JavaScript WebAPI、DOM、事件和操作元素实例详解

    目录 WebAPI DOM DOM树 DOM获取元素方式 document对象属性 事件 事件的使用步骤 事件的类型 操作元素 操作元素内容 操作元素属性 操作元素样式 排他思想 H5自定义属性 总结 WebAPI API:应用程序编程接口,是一些预先定义的函数,由某个软件开放给开发人员使用的,帮助开发者实现某种功能,开发人员无须访问源码.无须理解其内部工作机制细节,只需知道如何使用即可 简单理解: API 是给程序员提供的一种工具,以便能更轻松的实现想要完成的功能 WebAPI:主要针对浏览器

  • MutationObserver在页面水印实现起到的作用详解

    目录 背景 实现水印 恶意修改 MutationObserver 背景 大家平时在开发中或者在面试中,难免都会遇到一个问题——给页面加水印,其实这并不难,但是也是有一些注意点的,所以说看似简单的功能,要尽力做到: 1.严谨性 2.安全性 实现水印 其实实现水印并不难,只需要利用自定义指令 + canvas + background-image即可,实现起来也非常方便: import type { Directive, App } from 'vue' interface Value { font

  • Dom-api MutationObserver使用方法详解

    目录 1. 概述 2. 基本使用 2.1 observer 方法 2.2 MutationObserverInit 对象 2.3 disconnect()方法 2.4 takeRecords 3. MutationRecord 3.1 MutationRecord 实例 4. MutationObserver 实战 总结 1. 概述 MutationObserver 接口提供了监视对 DOM 树所做更改的能力.它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Ev

  • jQuery遍历DOM元素与节点方法详解

    本文实例讲述了jQuery遍历DOM元素与节点方法.分享给大家供大家参考,具体如下: 一.向上遍历--祖先元素 ① $(selector).parent([filter]):返回selector匹配元素的直接父元素,方法可以接受一个过滤selector来过滤返回的父元素. ② $(selector).parents([filter]):返回匹配元素的所有祖先节点,一直向上直到文档根元素html,方法可以接受一个过滤selector来过滤返回的祖先节点. 备注:parent与parents的区别,

  • 微信小程序3种位置API的使用方法详解

    获取位置 获取当前的地理位置.速度.当用户离开小程序后,此接口无法调用:当用户点击"显示在聊天顶部"时,此接口可继续调用. wx.getLocation(object) <view class="container"> <button bindtap='getLocation'>获取位置</button> <view wx:if="{{latitude !=''}}"> <view>纬度

  • js基础之DOM中document对象的常用属性方法详解

    -----引入 每个载入浏览器的 HTML 文档都会成为 Document 对象. Document 对象使我们可以从脚本中对 HTML 页面中的所有元素进行访问. 属性 1  document.anchors  返回对文档中所有 Anchor 对象的引用.还有document.links/document.forms/document.images等 2  document.URL       返回当前文档的url 3  document.title       返回当前文档的标题 4  do

  • js基础之DOM中元素对象的属性方法详解

    在 HTML DOM (文档对象模型)中,每个部分都是节点. 节点是DOM结构中最基本的组成单元,每一个HTML标签都是DOM结构的节点. 文档是一个    文档节点 . 所有的HTML元素都是    元素节点 所有 HTML 属性都是    属性节点 文本插入到 HTML 元素是    文本节点 注释是    注释节点. 最基本的节点类型是Node类型,其他所有类型都继承自Node,DOM操作往往是js中开销最大的部分,因而NodeList导致的问题最多.要注意:NodeList是'动态的',

  • JavaScript实现的DOM树遍历方法详解【二叉DOM树、多叉DOM树】

    本文实例讲述了JavaScript实现的DOM树遍历方法.分享给大家供大家参考,具体如下: 二叉 DOM 树的遍历 function Tree() { var Node = function(key){ this.key = key; this.left = null; this.right = null; } root =null; } 前序遍历 首先访问根结点,然后遍历左子树,最后遍历右子树 Tree.prototype.preOrderTraverse = function(callbac

  • 在Django下测试与调试REST API的方法详解

    对于大多数研发人员来说,都期望能找到一个良好的测试/调试方法,来提高工作效率和快速解决问题.所谓调试,偏重于对某个bug的查找.定位.修复:所谓测试,是检验某个功能是否达到预期效果.测试发现问题后进行调试,从而解决问题. 对于后台研发来说,往往没有客户端研发(Windows/Android等等)那样简单有效的DEBUG方法,比如Step by Step.虽然目前有很多IDE可以实现本地调试,但是因为后台研发的环境复杂,你很难在一台机器上模拟所有的环境,比如线上的数据库只能在内网访问等等,所以很多

  • 对python借助百度云API对评论进行观点抽取的方法详解

    通过百度云API接口抽取得到产品评论的观点,也掠去了很多评论中无用的内容以及符号,为后续进行文本主题挖掘或者规则的提取提供基础. 工具 1.百度云账号,申请应用接口(自然语言处理) 2.python3.5 以下是百度接口提供的说明: 我们使用到的可选值是13,kindle属于3C产品. 下面是代码示例: from aip import AipNlp import csv import pandas as pd from pandas.core.frame import DataFrame "&q

  • Laravel5.5+ 使用API Resources快速输出自定义JSON方法详解

    从Laravel 5.5+开始,加入了API Resources这个概念. 我们先来看一下官网如何定义这个概念的: When building an API, you may need a transformation layer that sits between your Eloquent models and the JSON responses that are actually returned to your application's users. Laravel's resour

  • Java实现API sign签名校验的方法详解

    目录 1. 前言 2. 签名生成策略 3. API 签名算法 Java 实现 4. 测试一下 1. 前言 目的:为防止中间人攻击. 场景: 项目内部前后端调用,这种场景只需要做普通参数的签名校验和过期请求校验,目的是为了防止攻击者劫持请求 url 后非法请求接口. 开放平台向第三方应用提供能力,这种场景除了普通参数校验和请求过期校验外,还要考虑 3d 应用的授权机制,不被授权的应用就算传入了合法的参数也不能被允许请求成功. 2. 签名生成策略 接下来详述场景 2,其实场景 1 也包含在场景 2

随机推荐