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
      12
      let 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

2、回调与MutationRecord

  • 每个回调都会收到一个MutationRecord实例的数组。
  • MutationRecord实例包含的信息包括发生了什么变化,以及DOM的哪一部分受到了影响。
  • 因为回调执行之前可能同时发生多个满足观察条件的事件,所以每次执行回调都会传入一个包含按顺序入队的MutationRecord实例的数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//反映一个属性变化的 MutationRecord 实例的数组
let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords));
observer.observe(document.body, { attributes: true });
document.body.setAttribute('foo', 'bar');
// [
// {
// addedNodes: NodeList [],
// attributeName: "foo",
// attributeNamespace: null,
// nextSibling: null,
// oldValue: null,
// previousSibling: null
// removedNodes: NodeList [],
// target: body
// type: "attributes"
// }
// ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//一次涉及命名ቆ间的类似变化
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
observer.observe(document.body, { attributes: true });
document.body.setAttributeNS('baz', 'foo', 'bar');
// [
// {
// addedNodes: NodeList [],
// attributeName: "foo",
// attributeNamespace: "baz",
// nextSibling: null,
// oldValue: null,
// previousSibling: null
// removedNodes: NodeList [],
// target: body
// type: "attributes"
// }
// ]
1
2
3
4
5
6
7
8
9
/*
连续修改会生成多个Mutati onRecord实例,下次回调执行时就会收到包含所有这些实例的数组,顺序为变化事件发生的顺序:
*/
let 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';
// [MutationRecord, MutationRecord, MutationRecord]
  • MutationRecord实例的属性:
  • 传给回调函数的第二个参数是观察变化的 MutationObserver 的实例。
1
2
3
4
5
let observer = new MutationObserver( (mutationRecords, mutationObserver) => console.log(mutationRecords,
mutationObserver));
observer.observe(document.body, { attributes: true });
document.body.className = 'foo';
// [MutationRecord], MutationObserver

3、disconnect()方法

  • 默认情况下,只要被观察的元素不被垃圾回收,MutationObserver的回调就会响应DOM变化事件,从而被执行。
  • disconnect()方法可以用来提前终止执行回调。
    • 不仅会停止此后变化事件的回调,也会抛弃已经加入任务队列要异步执行的回调。
1
2
3
4
5
6
let observer = new MutationObserver(() => console.log('<body> attributes changed')); 
observer.observe(document.body, { attributes: true });
document.body.className = 'foo';
observer.disconnect();
document.body.className = 'bar';
//(没有日志输出)
  • 要想让已经加入任务队列的回调执行,可以使用 setTimeout()让已经入列的回调执行完毕再调用disconnect()。
1
2
3
4
5
6
7
8
let observer = new MutationObserver(() => console.log('<body> attributes changed')); 
observer.observe(document.body, { attributes: true });
document.body.className = 'foo';
setTimeout(() => {
observer.disconnect();
document.body.className = 'bar';
}, 0);
// <body> attributes changed

4、复用MutationObserver

  • 多次调用 observe()方法,可以复用一个 MutationObserver 对象观察多个不同的目标节点。
  • MutationRecord 的 target 属性可以标识发生变化事件的目标节点。
  • disconnect()方法是一个“一刀切”的方案,调用它会停止观察所有目标。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords.map((x) => 
x.target)));
// 向页面主体添加两个子节点
let childA = document.createElement('div'),
childB = document.createElement('span');
document.body.appendChild(childA);
document.body.appendChild(childB);
// 观察两个子节点
observer.observe(childA, { attributes: true });
observer.observe(childB, { attributes: true });
// 修改两个子节点的属性
childA.setAttribute('foo', 'bar');
childB.setAttribute('foo', 'bar');
// [<div>, <span>]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords.map((x) => 
x.target)));
// 向页面主体添加两个子节点
let childA = document.createElement('div'),
childB = document.createElement('span');
document.body.appendChild(childA);
document.body.appendChild(childB);
// 观察两个子节点
observer.observe(childA, { attributes: true });
observer.observe(childB, { attributes: true });
observer.disconnect();
// 修改两个子节点的属性
childA.setAttribute('foo', 'bar');
childB.setAttribute('foo', 'bar');
// (没有日志输出)

5、重用MutationObserver

  • 调用disconnect()并不会结束MutationObserver的生命。还可以重新使用这个观察者,再将它关联到新的目标节点。
  • 下面的示例在两个连续的异步块中先断开然后又恢复了观察者与<body>元素的关联:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    let 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
2
3
4
5
6
7
8
9
10
let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords)); 
observer.observe(document.body, { attributes: true });
// 添加属性
document.body.setAttribute('foo', 'bar');
// 修改属性
document.body.setAttribute('foo', 'baz');
// 移除属性
document.body.removeAttribute('foo');
// 以上变化都被记录下来了
// [MutationRecord, MutationRecord, MutationRecord]
  • 把attributes设置为true的默认行为是观察所有属性,但不会在MutationRecord对象中记录原来的属性值。
  • 如果想观察某个或某几个属性,可以使用attributeFilter属性来设置白名单,即一个属性名字符串数组;
1
2
3
4
5
6
7
8
let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords)); 
observer.observe(document.body, { attributeFilter: ['foo'] });
// 添加白名单属性
document.body.setAttribute('foo', 'bar');
// 添加被排除的属性
document.body.setAttribute('baz', 'qux');
// 只有 foo 属性的变化被记录了
// [MutationRecord]
  • 如果想在变化记录中保存属性原来的值,可以将attributeOldValue属性设置为true。
1
2
3
4
5
6
7
let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords.map((x) => x.oldValue))); 
observer.observe(document.body, { attributeOldValue: true });
document.body.setAttribute('foo', 'bar');
document.body.setAttribute('foo', 'baz');
document.body.setAttribute('foo', 'qux');
// 每次变化都保留了上一次的值
// [null, 'bar', 'baz']

2、观察字符数据

  • MutationObserver 可以观察文本节点(如 Text、 Comment或ProcessingInstruction节点)中字符的添加、删除和修改。
  • 要为字符数据注册回调,需要在MutationObserverInit对象中将characterData 属性设置为true。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let 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
2
3
4
5
6
7
8
9
let observer = new MutationObserver( 
(mutationRecords) => console.log(mutationRecords.map((x) => x.oldValue)));
document.body.innerText = 'foo';
observer.observe(document.body.firstChild, { characterDataOldValue: true });
document.body.innerText = 'foo';
document.body.innerText = 'bar';
document.body.firstChild.textContent = 'baz';
// 每次变化都保留了上一次的值
// ["foo", "foo", "bar"]

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
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
// 清空主体
document.body.innerHTML = '';
let observer = new MutationObserver(
(mutationRecords) => console.log(mutationRecords));
// 创建两个初始子节点
document.body.appendChild(document.createElement('div'));
document.body.appendChild(document.createElement('span'));
observer.observe(document.body, { childList: true });
// 交换子节点顺序
document.body.insertBefore(document.body.lastChild, document.body.firstChild);
// 发生了两次变化:第一次是节点被移除,第二次是节点被添加
// [
// {
// addedNodes: NodeList[],
// attributeName: null,
// attributeNamespace: null,
// oldValue: null,
// nextSibling: null,
// previousSibling: div,
// removedNodes: NodeList[span],
// target: body,
// type: childList,
// },
// {
// addedNodes: NodeList[span],
// attributeName: null,
// attributeNamespace: null,
// oldValue: null,
// nextSibling: div,
// previousSibling: null,
// removedNodes: NodeList[],
// target: body,
// type: "childList",
// }
// ]

4、观察子树

  • 默认情况下,MutationObserver 将观察的范围限定为一个元素及其子节点的变化。
  • 可以把观察的范围扩展到这个元素的子树(所有后代节点),这需要在MutationObserverInit对象中将subtree属性设置为true。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 清空主体
document.body.innerHTML = '';
let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords));
// 创建一个后代
document.body.appendChild(document.createElement('div'));
// 观察<body>元素及其子树
observer.observe(document.body, { attributes: true, subtree: true });
// 修改<body>元素的子树
document.body.firstChild.setAttribute('foo', 'bar');
// 记录了子树变化的事件
// [
// {
// addedNodes: NodeList[],
// attributeName: "foo",
// attributeNamespace: null,
// oldValue: null,
// nextSibling: null,
// previousSibling: null,
// removedNodes: NodeList[],
// target: div,
// type: "attributes",
// }
// ]
  • 被观察子树中的节点被移出子树之后仍然能够触发变化事件。
  • 在子树中的节点离开该子树后,即使严格来讲该节点已经脱离了原来的子树,但它仍然会触发变化事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 清空主体
document.body.innerHTML = '';
let observer = new MutationObserver( (mutationRecords) => console.log(mutationRecords));
let subtreeRoot = document.createElement('div'),
subtreeLeaf = document.createElement('span');
// 创建包含两层的子树
document.body.appendChild(subtreeRoot);
subtreeRoot.appendChild(subtreeLeaf);
// 观察子树
observer.observe(subtreeRoot, { attributes: true, subtree: true });
// 把节点转移到其他子树
document.body.insertBefore(subtreeLeaf, subtreeRoot);
subtreeLeaf.setAttribute('foo', 'bar');
// 移出的节点仍然触发变化事件
// [MutationRecord]

异步回调与记录队列

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

1、记录队列

  • 每次MutationRecord被添加到MutationObserver的记录队列时,仅当之前没有已排期的微任务回调时(队列中微任务长度为0),才会将观察者注册的回调(在初始化Mutation0bserver时传入)作为微任务调度到任务队列上。
  • 不过在回调的微任务异步执行期间,有可能又会发生更多变化事件。因此被调用的回调会接收到一个MutationRecord实例的数组,顺序为它们进入记录队列的顺序。回调要负责处理这个数组的每一个实例,因为函数退出之后这些实现就不存在了。回调执行后,这些MutationRecord就用不着了,因此记录队列会被清空,其内容会被丢弃。

2、takeRecords()方法

  • 调用MutationObserver实例的takeRecords()方法可以清空记录队列,取出并返回其中的所有MutationRecord实例。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let 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。