JavaScript中MutationObServer监听DOM元素详解

晴空闲云 等级 70 0 0

DOM的MutationObServer接口,可以在DOM被修改时异步执行回调函数,我的理解就是可以监听DOM修改。

基本使用

可以通过MutationObserver构造函数实例化,参数是一个回调函数。

let observer = new MutationObserver(() => console.log("change"));
console.log(observer);

observer对象原型链如下:

JavaScript中MutationObServer监听DOM元素详解

可以看到有disconnect、observer、takeRecords方法。

1. observer方法监听

observer方法用于关联DOM元素,并根据相关设置进行监听。

语法如下:

// 接收两个参数
observer(DOM元素, MutationObserverInit对象);

其中:

1. 第一个参数DOM元素就是页面元素,比如:body、div等。
2. 第二个参数就是设置要监听的范围。比如:属性、文本、子节点等,是一个键值对数组。

示例1,监听body元素class的变化:

let observer = new MutationObserver(() => console.log("change"));
// 监听body元素的属性变化
observer.observe(document.body, {
    attributes: true
});
// 更改body元素的class,会异步执行创建MutationObserver对象时传入的回调函数
document.body.className = "main";
console.log("修改了body属性");
// 控制台输出:
//    修改了body属性
//    change

上面 change 的输出是在 修改了body属性 之后,可见注册的回调函数是异步执行的,是在后面执行的。

2. 回调函数增加MutationRecord实例数组参数

现在回调函数非常简单,就是输出一个字符串,看不出到底发生了什么变化。

其实回调函数接收一个 MutationRecord 实例数组,实务中可以通过这个查看详细的信息。

let observer = new MutationObserver(
    // 回调函数是一个 MutationRecord 实例数组。格式如下:
    //     [MutationRecord, MutationRecord, MutationRecord, ...]
    (mutationRecords) => console.log(mutationRecords)
);
observer.observe(document.body, {
    attributes: true
});
document.body.className = "main";
console.log("修改了body属性");
// 控制台输出:
//    修改了body属性
//     (1) [MutationRecord]

其中 mutationRecords信息 如下:

JavaScript中MutationObServer监听DOM元素详解

其中几个比较关键的信息:

attributeName 表示修改的属性名称
target 修改的目标
type 类型

如果多次修改body的属性,那么会有多条记录:

// MutationRecord
let observer = new MutationObserver(
    // 回调函数接收一个 MutationRecord 实例,是一个数组。
    (mutationRecords) => console.log(mutationRecords)
);
observer.observe(document.body, {
    attributes: true
});

// 修改三次
document.body.className = "main";
document.body.className = "container";
document.body.className = "box";

// 控制台打印如下:
//     (3) [MutationRecord, MutationRecord, MutationRecord]

注意:

这里不是修改一次就执行一次回调,而是每修改一次就往 mutationRecords 参数加入一个 MutationRecord 实例,最后执行一次回调打印出来。

如果修改一次就执行一次回调,那么性能就会比较差。

3. disconnect方法终止回调

如果要终止回调,可以使用disconnect方法。

let observer = new MutationObserver(
    (mutationRecords) => console.log(mutationRecords)
);
observer.observe(document.body, {
    attributes: true
});

// 第一次修改
document.body.className = "main";

// 终止
observer.disconnect();

// 第二次修改
document.body.className = "container";

// 没有日志输出

这里没有日志输出,包括第一次修改也没有日志输出,因为回调函数的执行是异步的,是在最后执行的。后面把observer终止了,所以就不会执行了。

可以用setTimeout控制最后才终止,这样回调就会正常执行。

let observer = new MutationObserver(
    (mutationRecords) => console.log(mutationRecords)
);
observer.observe(document.body, {
    attributes: true
});

// 第一次修改
document.body.className = "main";

// 终止
setTimeout(() => {
    observer.disconnect();
    // 第三次修改,下面修改不会回调了
    document.body.className = "container";
}, 0);

// 第二次修改
document.body.className = "container";

// 页面输出:
//    (2) [MutationRecord, MutationRecord]

终止之后再启用

终止了之后可以再次启动,请看下面示例:

let observer = new MutationObserver(
    (mutationRecords) => console.log(mutationRecords)
);
observer.observe(document.body, {
    attributes: true
});

// 第一次修改,会入 mutationRecords 数组
document.body.className = "main";

// 终止
setTimeout(() => {
    observer.disconnect();
    // 第二次修改,因为终止了,下面修改不会入 mutationRecords 数组
    document.body.className = "container";
}, 0);

setTimeout(() => {
    // 再次启用
    observer.observe(document.body, {
        attributes: true
    });
    // 修改body属性,会入 mutationRecords 数组
    document.body.className = "container";
}, 0);

// 控制台输出:
//    [MutationRecord]
//    [MutationRecord]

这边回调函数是执行了两次,打印了两个,其中:

第一个输出是在第一次修改,后面没有同步代码了,就执行了回调。
第二个输出是在第三次修改,因为重新启用了,所以就正常执行了回调。

第二次修改,因为observer被终止了,所以修改body的属性不会入 mutationRecords 数组。

4. takeRecords方法获取修改记录

如果希望在终止observer之前,对已有的 mutationRecords 记录进行处理,可以用takeRecords方法获取。

let observer = new MutationObserver(
    (mutationRecords) => console.log(mutationRecords)
);
observer.observe(document.body, {
    attributes: true
});

// 第一次修改,会入 mutationRecords 数组
document.body.className = "main";
// 第二次修改,会入 mutationRecords 数组
document.body.className = "container";
// 第三次修改,会入 mutationRecords 数组
document.body.className = "box";

// 取到修改记录,可以对其进行处理
let mutationRecords =  observer.takeRecords();
console.log(mutationRecords); 
// 控制台打印:
//     (3) [MutationRecord, MutationRecord, MutationRecord]
console.log(observer.takeRecords());
// 控制台打印:
//    []

// 终止
observer.disconnect();

监听多个元素

上面监听都是只有一个元素,如果要监听多个元素可以复用MutationObserver实例。

let observer = new MutationObserver(
    (mutationRecords) => console.log(mutationRecords)
);
// 创建 div1 元素,并监听
let div1 = document.createElement("div");
observer.observe(div1, {
    attributes: true
});
div1.id = "box1";

// 创建div2并监听
let div2 = document.createElement("div");
observer.observe(div2, {
    attributes: true
});
div2.id = "box2";

// 控制台打印:
//    (2) [MutationRecord, MutationRecord]

控制台打印了两个MutationRecord,其中:

第一个 MutationRecord 就是 div1 的id属性修改记录。
第二个 MutationRecord 就是 div2 的id属性修改记录。

其他使用方式和上面的类似。

监听范围MutationObserverInit对象

上面的监听都是监听属性,当然也可以监听其他的东西,比如:文本、子节点等。

1. 观察属性

上面的例子都是观察元素自有的属性,这里再举一个自定义属性的例子。

let observer = new MutationObserver(
    (mutationRecords) => console.log(mutationRecords)
);
observer.observe(document.body, {
    attributes: true
});
// 修改自定义的属性
document.body.setAttribute("data-id", 1);

// 控制台打印:
//    [MutationRecord]

修改自定义的属性一样会加入到 mutationRecords 数组。

另外值的一提的是 data-id 经常用来给元素标记一些数据啥的,如果发生变化,程序就可以监听到,就可以处理一些相应的逻辑。

attributeFilter过滤

如果要监听指定的属性变化,可以用 attributeFilter 过滤。

let observer = new MutationObserver(
    (mutationRecords) => console.log(mutationRecords)
);
observer.observe(document.body, {
    attributes: true,
    // 设置白名单
    attributeFilter: ["data-id"]
});

// 修改白名单 attributeFilter 内的属性,会入 mutationRecords
document.body.setAttribute("data-id", 1);

// 修改不在白名单 attributeFilter 内的属性,不会入 mutationRecords
document.body.setAttribute("class", "main");

// 控制台打印:
//    [MutationRecord]

attributeOldValue记录旧值

如果要记录旧值,可以设置 attributeOldValue 为true。

let observer = new MutationObserver(
    // MutationRecord对象中oldValue表示旧值
    (mutationRecords) => console.log(mutationRecords.map((x) => x.oldValue))
);
observer.observe(document.body, {
    attributes: true,
    attributeOldValue: true,
});
// 第一次修改,因为原来没有值,所以旧值 oldValue = null
document.body.setAttribute("class", "main");
// 第二次修改,因为前面有改了一次,所以旧值 oldValue = main
document.body.setAttribute("class", "container");

// 控制台打印:
//    (2) [null, 'main']

2. 观察文本

观察文本设置 characterData 为 true 即可,不过只能观察文本节点。

请看如下示例:

<!-- 一个性感的div -->
<div id="box">Hello</div>

<script type="text/javascript">
    let observer = new MutationObserver(
        (mutationRecords) => console.log(mutationRecords)
    );

    // 获取文本节点
    let textNode = document.getElementById("box").childNodes[0];
    observer.observe(textNode, {
        // 观察文本变化
        characterData: true
    });
    // 修改文本
    textNode.textContent = "Hi";

    // 控制台打印:
    //    [MutationRecord]
</script>

如果直接监听div元素,那么是不生效的:

<!-- 一个性感的div -->
<div id="box">Hello</div>

<script type="text/javascript">
    let observer = new MutationObserver(
        (mutationRecords) => console.log(mutationRecords)
    );

    // 监听div不会生效
    let box = document.getElementById("box");
    observer.observe(box, {
        characterData: true
    });
    box.textContent = "Hi";

    // 控制台无输出
</script>

characterDataOldValue记录旧值

如果要记录文本旧值,可以设置 characterDataOldValue 为true。

<!-- 一个性感的div -->
<div id="box">Hello</div>

<script type="text/javascript">
    let observer = new MutationObserver(
        (mutationRecords) => console.log(mutationRecords.map((x) => x.oldValue))
    );

    // 获取文本节点
    let textNode = document.getElementById("box").childNodes[0];
    observer.observe(textNode, {
        // 观察文本变化
        characterData: true,
        // 保留旧数据
        characterDataOldValue: true,
    });
    // 修改文本两次
    textNode.textContent = "Hi";
    textNode.textContent = "Nice";

    // 控制台打印:
    //    (2) ['Hello', 'Hi']
</script>

因为div内的内容原本为Hello,先修改为Hi,又修改为Nice,所以两次修改的旧值就为:Hello 和 Hi 了。

3. 观察子节点

MutationObserver 实例也可以观察目标节点子节点的变化。

<!-- 一个性感的div -->
<div id="box">Hello</div>

<script type="text/javascript">
    let observer = new MutationObserver(
        (mutationRecords) => console.log(mutationRecords)
    );

    // 获取div
    let box = document.getElementById("box");
    observer.observe(box, {
        // 观察子节点变化
        childList: true,
    });
    // 添加元素
    let span = document.createElement("span")
    span.textContent = "world";
    box.appendChild(span);

    // 控制台打印:
    //    [MutationRecord]
</script>

MutationRecord中的addedNodes属性记录了增加的节点。

移除节点

<!-- 一个性感的div -->
<div id="box">Hello</div>

<script type="text/javascript">
    let observer = new MutationObserver(
        (mutationRecords) => console.log(mutationRecords)
    );

    // 获取div
    let box = document.getElementById("box");
    observer.observe(box, {
        // 观察子节点变化
        childList: true,
    });
    // 移除第一个子节点,就是Hello文本节点
    box.removeChild(box.childNodes[0]);

    // 控制台打印:
    //    [MutationRecord]
</script>

MutationRecord中的removedNodes属性记录了移除的节点。

移动节点

对于已有的节点进行移动,那么会记录两条MutationRecord记录,因为移动现有的节点是先删除,后添加。

<!-- 一个性感的div -->
<div id="box">Hello<span>world</span></div>

<script type="text/javascript">
    let observer = new MutationObserver(
        (mutationRecords) => console.log(mutationRecords)
    );

    // 获取div
    let box = document.getElementById("box");
    observer.observe(box, {
        // 观察子节点变化
        childList: true,
    });
    // 将span节点移动到Hello节点前面
    box.insertBefore(box.childNodes[1], box.childNodes[0]);
    // 移动节点,实际是先删除,后添加。

    // 控制台打印:
    //    (2) [MutationRecord, MutationRecord]
</script>

4. 观察子树

上面观察的节点都是当前设置的目标节点,比如body,就只能观察body元素和其子节点的变化。

如果要观察body及其所有后代节点的变化,那么可以设置subtree属性为true。

<!-- 一个性感的div -->
<div id="box">Hello<span>world</span></div>

<script type="text/javascript">
    let observer = new MutationObserver(
        (mutationRecords) => console.log(mutationRecords)
    );

    let box = document.getElementById("box");
    observer.observe(box, {
        attributes: true,
        // 观察子树的变化
        subtree: true
    });
    // span元素的id属性变化就可以观察到
    box.childNodes[1].id = "text";
    // 控制台打印:
    //    [MutationRecord]
</script>

subtree设置为true后,不光div元素本身,span元素也可以观察到了。

总结

  1. MutationObserver实例可以用来观察对象。
  2. MutationRecord实例记录了每一次的变化。
  3. 回调函数需要所有脚本任务完成后,才会执行,即采用异步方式。
  4. 可以观察的访问有属性、文本、子节点、子树。
收藏
评论区

相关推荐

对 JavaScript 中事件循环的理解​
一、是什么 JavaScript 在设计之初便是单线程,即指程序运行时,只有一个线程存在,同一时间只能做一件事 为什么要这么设计,跟JavaScript的应用场景有关 JavaScript 初期作为一门浏览器脚本语言,通常用于操作 DOM ,如果是多线程,一个线程进行了删除 DOM ,另一个添加 DOM,此时浏览器该如何处理? 为了解决单
javaScript. Dom 基本操作
DOM节点查找jsdocument.getElementById() //通过id查找document.getElementsByTagName() //通过标签名document.getElementsByName() //通过name名查找 document.getElementsByClassName("类名")//通过类名获取元素对象 documen
nextTick原理解析
nextTick 是什么\$nextTick:根据官方文档的解释,它可以在 DOM 更新完毕之后执行一个回调函数,并返回一个 Promise(如果支持的话)js// 修改数据vm.msg "Hello";// DOM 还没有更新Vue.nextTick(function() // DOM 更新了);这块理解 EventLoop 的应该一看就懂,其实就是
03. react 初次见面
    与浏览器的 DOM 元素不同,React 当中的元素事实上是普通的对象,React DOM 可以确保 浏览器 DOM 的数据内容与 React 元素保持一致。 1、将元素渲染到 DOM 中 --------------     首先我们在一个 HTML 页面中添加一个 id="root" 的 <div>: <div id="root">
DOM
跨站脚本攻击(Cross-site scripting,通常简称为XSS)发生在客户端,可被用于进行窃取隐私、钓鱼欺骗、偷取密码、传播恶意代码等攻击行为。可以分为反射型、存储型、DOM型等,本文主要讲解**DOM-XSS漏洞挖掘与攻击面延申**。接下来就开门见山,讲解干货吧。 **DOM-XSS典型应用场景*
DOM查询
1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type"
JavaScript DOM初学笔记
1\. DOM简介 ========= 1\. 1 什么是DOM ------------ **文档对象模型**(**Document Object Model**,简称 DOM),是 W3C组织推荐的处理可扩展标记语言(HTML 或者标准编程接口。 1.2 DOM树 -------- ![HTML DOM Node Tree](https://st
JavaScript基础
**_1,js组成_** ------------ ###         核心:ECMAScript标准                   此标准指定了js的基础语法,数据类型,关键字,操作符,对象; ###         DOM:文档对象模型标准(Document object Model)                   是js针对xm
JavaScript的BOM和DOM
1,window对象,所有浏览器都支持window对象,它表示浏览器窗口 BOM(browser Object Model)是指浏览器对象模型,它使JavaScript有能力与浏览器进行"对话". DOM(Document Object Model)是指文档对象类型,通过它,可以访问HTML文档的所有元素 window对象客户端JavaScript最高
Jquery元素追加和删除
**介绍**    DOM是Document Object Modeule的缩写,一般来说,DOM操作分成3个方面。 **1、DOM Core**     DOM Core并不专属于javascript,任何一种支持DOM的程序设计语言都可以使用它,用途也远不止仅限于网页,也可以用来处理任何一种使用标记语言编写出来的文档,如XML。     例如:
React 入门及学习笔记
React 构建用户界面的JavaScript库,主要用于构建UI界面。Instagram,2013年开源。 特点: --- 1. 声明式的设计 2. 高效,采用虚拟DOM来实现DOM的渲染,最大限度的减少DOM的操作。 3. 灵活,跟其他库灵活搭配使用。 4. JSX,俗称JS里面写HTML,JavaScript语法的扩展。 5. 组件化,模
React的虚拟DOM
上一篇文章中,DOM树的信息可以用JavaScript对象来表示,反过来,可以根据这个用JavaScript对象表示的树结构来真正构建一颗DOM树。 用JavaScript对象表示DOM信息和结构,当状态变更的时候,重新渲染这个JavaScript的对象结构,当然这样做,其实并没有更新到真正的页面上。 但是可以用新渲染的对象树和旧的树进行对比,记录这两棵树
React篇(005)
答案: 1、**React 速度很快**:它并不直接对 DOM 进行操作,引入了一个叫做虚拟 DOM 的概念,安插在 javascript 逻辑和实际的 DOM 之间,性能好。 2、**跨浏览器兼容**:虚拟DOM帮助我们解决了跨浏览器问题,它为我们提供了标准化的 API,甚至在 IE8 中都是没问题的。 3、**一切都是 co
Vue diff 算法
一、虚拟 DOM (virtual dom) ----------------------   diff 算法首先要明确一个概念就是 diff 的对象是虚拟DOM(virtual dom),更新真实 DOM 是 diff 算法的结果。   注:virtual dom 可以看作是一个使用 JavaScript 模拟了 DOM结构 的树形结构,这个树结构包含
JavaScript中MutationObServer监听DOM元素详解
DOM的MutationObServer接口,可以在DOM被修改时异步执行回调函数,我的理解就是可以监听DOM修改。 基本使用可以通过MutationObserver构造函数实例化,参数是一个回调函数。jslet observer new MutationObserver(() console.log("change"));console.log(obs