JavaScript学习笔记(三十六)DOM(三)
MutationObserver接口
- MutationObserver接口,可以在DOM被修改时异步执行回调。
- Mutati onObserver可以观察整个文档、DOM树的一部分, 或某个元素。此外还可以观察元素属性、子节点、文本,或者前ʻ者任意组合的变化。
基本用法
- MutationObserver的实例要通过调用MutationObserver构造函数并传人一个回调函数来创建。
1
let observer = new MutationObserver(() => console.log('DOM was mutated!'));
1、observe方法
- 新创建的MutationObserver实例不会关联DOM的任何部分。要把这个observer与DOM关联起来,需要使用observe()方法。
- 方法接收两个必需的参数:要观察其变化的DOM节点和一个MutationObserverInit对象。
- MutationObserverInit对象用于控制观察哪些方面的变化,是一个键/值对形式配置选项的字典。
1
2
3
4
5
6
7
8
9
10
11
12let observer = new MutationObserver(() => console.log('<body> attributes changed'));
observer.observe(document.body, { attributes: true });
/*
执行以上代码后,<body>元素上任何属性发生变化都会被这个MutationObserver实例发现,然后就会异步执行注册的回调函数。<body>元素后代的修改或其他非属性修改都不会触发回调进入任务队列。
*/
//通过以下代码来验证
let observer = new MutationObserver(() => console.log('<body> attributes changed'));
observer.observe(document.body, { attributes: true });
document.body.className = 'foo';
console.log('Changed body class');
// Changed body class
// <body> attributes changed
- MutationObserverInit对象用于控制观察哪些方面的变化,是一个键/值对形式配置选项的字典。
2、回调与MutationRecord
- 每个回调都会收到一个MutationRecord实例的数组。
- MutationRecord实例包含的信息包括发生了什么变化,以及DOM的哪一部分受到了影响。
- 因为回调执行之前可能同时发生多个满足观察条件的事件,所以每次执行回调都会传入一个包含按顺序入队的MutationRecord实例的数组。
1 | //反映一个属性变化的 MutationRecord 实例的数组 |
1 | //一次涉及命名ቆ间的类似变化 |
1 | /* |
- MutationRecord实例的属性:
- 传给回调函数的第二个参数是观察变化的 MutationObserver 的实例。
1 | let observer = new MutationObserver( (mutationRecords, mutationObserver) => console.log(mutationRecords, |
3、disconnect()方法
- 默认情况下,只要被观察的元素不被垃圾回收,MutationObserver的回调就会响应DOM变化事件,从而被执行。
- disconnect()方法可以用来提前终止执行回调。
- 不仅会停止此后变化事件的回调,也会抛弃已经加入任务队列要异步执行的回调。
1 | let observer = new MutationObserver(() => console.log('<body> attributes changed')); |
- 要想让已经加入任务队列的回调执行,可以使用 setTimeout()让已经入列的回调执行完毕再调用disconnect()。
1 | let observer = new MutationObserver(() => console.log('<body> attributes changed')); |
4、复用MutationObserver
- 多次调用 observe()方法,可以复用一个 MutationObserver 对象观察多个不同的目标节点。
- MutationRecord 的 target 属性可以标识发生变化事件的目标节点。
- disconnect()方法是一个“一刀切”的方案,调用它会停止观察所有目标。
1 | let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords.map((x) => |
1 | let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords.map((x) => |
5、重用MutationObserver
- 调用disconnect()并不会结束MutationObserver的生命。还可以重新使用这个观察者,再将它关联到新的目标节点。
- 下面的示例在两个连续的异步块中先断开然后又恢复了观察者与
<body>
元素的关联:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let observer = new MutationObserver(() => console.log('<body> attributes
changed'));
observer.observe(document.body, { attributes: true });
// 这行代码会触发变化事件
document.body.setAttribute('foo', 'bar');
setTimeout(() => {
observer.disconnect();
// 这行代码不会触发变化事件
document.body.setAttribute('bar', 'baz');
}, 0);
setTimeout(() => {
// Reattach
observer.observe(document.body, { attributes: true });
// 这行代码会触发变化事件
document.body.setAttribute('baz', 'qux');
}, 0);
// <body> attributes changed
// <body> attributes changed
MutationObserverInit与观察范围
- MutationObserverInit 对象用于控制对目标节点的观察范围。
- 粗略地讲,观察者可以观察的事件包括属性变化、文本变化和子节点变化。
- 下表列出了 MutationObserverInit 对象的属性:
- 在调用observe()时,MutationObserverInit 对象中的attribute、characterData和childList属性必须至少有一项为true(无论是直接设置这几个属性,还是通过设置attributeOldvalue等属性间接导致它们的值转换为true)。否则会抛出错误,因为没有任何变化事件可能触发回调。
1、观察属性
- MutationObserver可以观察节点属性的添加、移除和修改。
- 要为属性变化注册回调,需要在MutationObserverInit对象中将attributes属性设置为true。
1 | let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords)); |
- 把attributes设置为true的默认行为是观察所有属性,但不会在MutationRecord对象中记录原来的属性值。
- 如果想观察某个或某几个属性,可以使用attributeFilter属性来设置白名单,即一个属性名字符串数组;
1 | let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords)); |
- 如果想在变化记录中保存属性原来的值,可以将attributeOldValue属性设置为true。
1 | let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords.map((x) => x.oldValue))); |
2、观察字符数据
- MutationObserver 可以观察文本节点(如 Text、 Comment或ProcessingInstruction节点)中字符的添加、删除和修改。
- 要为字符数据注册回调,需要在MutationObserverInit对象中将characterData 属性设置为true。
1
2
3
4
5
6
7
8
9
10
11
12let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords));
// 创建要观察的文本节点
document.body.firstChild.textContent = 'foo';
observer.observe(document.body.firstChild, { characterData: true });
// 赋值为相同的字符串
document.body.firstChild.textContent = 'foo';
// 赋值为新字符串
document.body.firstChild.textContent = 'bar';
// 通过节点设置函数赋值
document.body.firstChild.textContent = 'baz';
// 以上变化都被记录下来了
// [MutationRecord, MutationRecord, MutationRecord] - 将characterData属性设置为true的默认行为不会在MutationRecord对象中记录原来的字符数据。如果想在变化记录中保存原来的字符数据,可以将characterData0ldValue属性设置为true。
1 | let observer = new MutationObserver( |
3、观察子节点
- MutationObserver 可以观察目标节点子节点的添加和移除。要观察子节点,需要在Mutation-ObserverInit对象中将childList属性设置为true。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41//1、下面的例子演示了添加子节点:
// 清空主体
document.body.innerHTML = '';
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
observer.observe(document.body, { childList: true });
document.body.appendChild(document.createElement('div'));
// [
// {
// addedNodes: NodeList[div],
// attributeName: null,
// attributeNamespace: null,
// oldValue: null,
// nextSibling: null,
// previousSibling: null,
// removedNodes: NodeList[],
// target: body,
// type: "childList",
// }
// ]
//2、下面的例子演示了移除子节点:
// 清空主体
document.body.innerHTML = '';
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
observer.observe(document.body, { childList: true });
document.body.appendChild(document.createElement('div'));
// [
// {
// addedNodes: NodeList[],
// attributeName: null,
// attributeNamespace: null,
// oldValue: null,
// nextSibling: null,
// previousSibling: null,
// removedNodes: NodeList[div],
// target: body,
// type: "childList",
// }
// ] - 对子节点重新排序(尽管调用一个方法即可实现)会报告两次变化事件,因为从技术上会涉及先移除和再添加。
1 | // 清空主体 |
4、观察子树
- 默认情况下,MutationObserver 将观察的范围限定为一个元素及其子节点的变化。
- 可以把观察的范围扩展到这个元素的子树(所有后代节点),这需要在MutationObserverInit对象中将subtree属性设置为true。
1 | // 清空主体 |
- 被观察子树中的节点被移出子树之后仍然能够触发变化事件。
- 在子树中的节点离开该子树后,即使严格来讲该节点已经脱离了原来的子树,但它仍然会触发变化事件。
1 | // 清空主体 |
异步回调与记录队列
- MutationObserver接口是出于性能考虑而设计的,其核心是异步回调与记录队列模型。
- 为了在大量变化事件发生时不影响性能,每次变化的信息(由观察者实例决定)会保存在MutationRecord实例中,然后添加到记录队列。
- 这个队列对每个MutationObserver实例都是唯一的,是所有 DOM变化事件的有序列表。
1、记录队列
- 每次MutationRecord被添加到MutationObserver的记录队列时,仅当之前没有已排期的微任务回调时(队列中微任务长度为0),才会将观察者注册的回调(在初始化Mutation0bserver时传入)作为微任务调度到任务队列上。
- 不过在回调的微任务异步执行期间,有可能又会发生更多变化事件。因此被调用的回调会接收到一个MutationRecord实例的数组,顺序为它们进入记录队列的顺序。回调要负责处理这个数组的每一个实例,因为函数退出之后这些实现就不存在了。回调执行后,这些MutationRecord就用不着了,因此记录队列会被清空,其内容会被丢弃。
2、takeRecords()方法
- 调用MutationObserver实例的takeRecords()方法可以清空记录队列,取出并返回其中的所有MutationRecord实例。
1
2
3
4
5
6
7
8
9let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords));
observer.observe(document.body, { attributes: true });
document.body.className = 'foo';
document.body.className = 'bar';
document.body.className = 'baz';
console.log(observer.takeRecords());
console.log(observer.takeRecords());
// [MutationRecord, MutationRecord, MutationRecord]
// []
性能、内存与垃圾回收
- DOM Level 2规范中描述的MutationEvent定义了一组会在各种DOM变化时触发的事件。
- 由于浏览器事件的实现机制,这个接口出现了严重的性能问题。因此,DOM Level 3规定废弃了这些事件。Mutationobserver接口就是为替代这些事件而设计的更实用、性能更好的方案。
- 将变化回调委托给微任务来执行可以保证事件同步触发,同时避免随之而来的混乱。
- 为MutationObserver而实现的记录队列,可以保证即使变化事件被爆发式地触发,也不会显著地拖慢浏览器。无论如何,使用Mutationobserver仍然不是没有代价的。
1、MutationObserver的引用
- Mutationobserver实例与目标节点之间的引用关系是非对称的。
- MutationObserver拥有对要观察的目标节点的弱引用。因为是弱引用,所以不会妨碍垃圾回收程序回收目标节点。
- 目标节点拥有对 Mutationobserver 的强引用。如果目标节点从 DOM中被移除,随后被垃圾回收,则关联的MutationObserver也会被垃圾回收。
2、MutationRecord的引用
- 记录队列中的每个MutationRecord 实例至少包含对已有 DOM节点的一个引用。
- 如果变化是childList类型,则会包含多个节点的引用。
- 记录队列和回调处理的默认行为是耗尽这个队列,处理每个MutationRecora,然后让它们超出作用域并被垃圾回收。
- 有时候可能需要保存某个观察者的完整变化记录。保存这些MutationRecord 实例,也就会保存它们引用的节点,因而会妨碍这些节点被回收。
- 如果需要尽快地释放内存,建议从每个MutationRecord中抽取出最有用的信息,然后保存到一个新对象中,最后抛弃MutationRecord。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 姚永坤的小窝!
评论