MutationObserver 接口

不久前添加到 DOM 规范中的 MutationObserver 接口,可以在 DOM 被修改时异步执行回调。

使用 MutationObserver 可以观察整个文档、DOM 树的一部分,或某个元素。

此外还可以观察元素属性、子节点、文本,或者前三者任意组合的变化。

开始观察 observe( )

要观察节点变化,需要用到observe()方法,

回调并非与实际的 DOM 变化同步执行。

  1. // 1、需要创建MutationObserver实例,创建时传入节点变化时的回调函数
  2. let observer = new MutationObserver((mutationRecords, mutationObserver) => console.log(mutationRecords,mutationObserver));
  3. // [MutationRecord], MutationObserver
  4. // 2、使用observe()方法,参数有2个,分别为要观察其变化的 DOM 节点,以及一个 MutationObserverInit 对象
  5. observer.observe(document.body, { attributes: true });
  6. // 3、执行以上代码后,<body>元素上任何属性发生变化都会被这个 MutationObserver 实例发现,然后就会异步执行注册的回调函数。
  7. // <body>元素后代的修改或其他非属性修改都不会触发回调进入任务队列。

回调与变化记录 MutationRecord

每个回调都会收到一个 MutationRecord 实例的数组,例包含的信息包括发生了什么变化,以及 DOM 的哪一部分受到了影响。

因为回调执行之前可能同时发生多个满足观察条件的事件,所以每次执行回调都会传入一个包含按顺序入队的 MutationRecord 实例的数组

  1. document.body.setAttribute('foo', 'bar');
  2. /*
  3. MutationRecord 实例的数组
  4. [
  5. {
  6. addedNodes: NodeList [], // 包含变化中添加节点的 NodeList
  7. attributeName: "foo", // 被修改属性的名字
  8. attributeNamespace: null, // 对于使用了命名空间的"attributes"类型的变化,这里保存被修改属性的名字
  9. nextSibling: null, // 回变化节点的后一个同胞 Node
  10. oldValue: null, // 被替代的值
  11. previousSibling: null, // 变化节点的前一个同胞 Node
  12. removedNodes: NodeList [], // 包含变化中删除节点的 NodeList
  13. target: body, // 被修改影响的目标节点
  14. type: "attributes" // 字符串,表示变化的类型:"attributes"、"characterData"或"childList"
  15. }
  16. ]
  17. */

连续修改会生成多个 MutationRecord 实例,下次回调执行时就会收到包含所有这些实例的数组,顺序为变化事件发生的顺序

  1. document.body.className = 'foo';
  2. document.body.className = 'bar';
  3. document.body.className = 'baz';
  4. // [MutationRecord, MutationRecord, MutationRecord]

停止观察disconnect( )

默认情况下,只要被观察的元素不被垃圾回收,MutationObserver 的回调就会响应 DOM 变化事件,从而被执行。

要提前终止执行回调,可以调用 disconnect()方法。

下面的例子演示了同步调用disconnect()之后,不仅会停止此后变化事件的回调,也会抛弃已经加入任务队列要异步执行的回调。

  1. observer.disconnect();
  2. document.body.className = 'bar';
  3. //(没有日志输出)
  4. // 要想让已经加入任务队列的回调执行,可以使用 setTimeout()让已经入列的回调执行完毕再调用disconnect()
  5. setTimeout(() => {
  6. observer.disconnect();
  7. document.body.className = 'bar';
  8. }, 0);
  9. // <body> attributes changed

复用 MutationObserver

多次调用 observe()方法,可以复用一个 MutationObserver 对象观察多个不同的目标节点。

此时,MutationRecord 的 target 属性可以标识发生变化事件的目标节点。

  1. let observer = new MutationObserver(
  2. (mutationRecords) => console.log(mutationRecords.map((x) => x.target)) // 只返回target被修改的目标
  3. );
  4. // 向页面主体添加两个子节点
  5. let childA = document.createElement('div'),
  6. childB = document.createElement('span');
  7. document.body.appendChild(childA);
  8. document.body.appendChild(childB);
  9. // 观察两个子节点
  10. observer.observe(childA, { attributes: true });
  11. observer.observe(childB, { attributes: true });
  12. // 修改两个子节点的属性
  13. childA.setAttribute('foo', 'bar');
  14. childB.setAttribute('foo', 'bar');
  15. // [<div>, <span>]
  16. // 调用它会停止观察所有目标
  17. observer.disconnect();

重新观察 observe( )

  1. let observer = new MutationObserver(() => console.log('<body> attributes changed'));
  2. observer.observe(document.body, { attributes: true });
  3. // 这行代码会触发变化事件
  4. document.body.setAttribute('foo', 'bar');
  5. setTimeout(() => {
  6. observer.disconnect();
  7. // 这行代码不会触发变化事件
  8. document.body.setAttribute('bar', 'baz');
  9. }, 0);
  10. setTimeout(() => {
  11. // 重新观察
  12. observer.observe(document.body, { attributes: true });
  13. // 这行代码会触发变化事件
  14. document.body.setAttribute('baz', 'qux');
  15. }, 0);
  16. // <body> attributes changed
  17. // <body> attributes changed

能观察什么 { }

  1. let observer = new MutationObserver(() => console.log('<body> attributes changed'));
  2. observer.observe(document.body, { attributes: true }); // 第二个参数就是能观察什么的MutationObserverInit 对象

默认都为false

subtree 布尔值,表示除了目标节点,是否观察目标节点的子树(后代)
如果是 false,则只观察目标节点的变化;如果是 true,则观察目标节点及其整个子树

attributes 布尔值,表示是否观察目标节点的属性变化

attributeFilter 字符串数组,表示要观察哪些属性的变化( { attributeFilter: [‘foo’] } )
把这个值设置为 true 也会将 attributes 的值转换为 true 默认为观察所有属性

attributeOldValue 布尔值,表示 MutationRecord 是否记录变化之前的属性值
把这个值设置为 true 也会将 attributes 的值转换为 true

characterData 布尔值,表示修改字符数据是否触发变化事件

characterDataOldValue 布尔值,表示 MutationRecord 是否记录变化之前的字符数据
把这个值设置为 true 也会将 characterData 的值转换为 true

childList 布尔值,表示修改目标节点的子节点是否触发变化事件

至少有一项为 true,否则会报错

清空记录队列并取出 takeRecords()

MutationObserver 接口是出于性能考虑而设计的,其核心是异步回调与记录队列模型。

为了在 大量变化事件发生时不影响性能,每次变化的信息(由观察者实例决定)会保存在 MutationRecord 实例中,然后添加到记录队列。

这在希望断开与观察目标的联系,但又希望处理由于调用 disconnect()而被抛弃的记录队列中的 MutationRecord 实例时比较有用

  1. let observer = new MutationObserver(
  2. (mutationRecords) => console.log(mutationRecords));
  3. observer.observe(document.body, { attributes: true });
  4. document.body.className = 'foo';
  5. document.body.className = 'bar';
  6. document.body.className = 'baz';
  7. console.log(observer.takeRecords());
  8. // [MutationRecord, MutationRecord, MutationRecord]
  9. console.log(observer.takeRecords());
  10. // []

性能、内存与垃圾回收

DOM Level 2 规范中描述的 MutationEvent 定义了一组会在各种 DOM 变化时触发的事件。

由于 浏览器事件的实现机制,这个接口出现了严重的性能问题。因此,DOM Level 3 规定废弃了这些事件。

MutationObserver 接口就是为替代这些事件而设计的更实用、性能更好的方案。

将变化回调委托给微任务来执行可以保证事件同步触发,同时避免随之而来的混乱。

为 MutationObserver 而实现的记录队列,可以保证即使变化事件被爆发式地触发,也不会显著地拖慢浏览器。

无论如何,使用 MutationObserver 仍然不是没有代价的。因此理解什么时候避免出现这种情况 就很重要了。

1. MutationObserver 的引用

MutationObserver 拥有对要 观察的目标节点的弱引用。因为是弱引用,所以不会妨碍垃圾回收程序回收目标节点。

然而,目标节点却拥有对 MutationObserver 的强引用。如果目标节点从 DOM 中被移除,随后 被垃圾回收,则关联的 MutationObserver 也会被垃圾回收。

2. MutationRecord 的引用

记录队列中的每个 MutationRecord 实例至少包含对已有 DOM 节点的一个引用。

如果变化是 childList 类型,则会包含多个节点的引用。

记录队列和回调处理的默认行为是耗尽这个队列,处理 每个 MutationRecord,然后让它们超出作用域并被垃圾回收。

有时候可能需要保存某个观察者的完整变化记录。

保存这些 MutationRecord 实例,也就会保存 它们引用的节点,因而会妨碍这些节点被回收。

如果需要尽快地释放内存,建议从每个 MutationRecord 中抽取出最有用的信息,然后保存到一个新对象中,最后抛弃 MutationRecord。

  1. let observer = new MutationObserver(
  2. (mutationRecords) => console.log(mutationRecords));
  3. observer.observe(document.body, { attributes: true });
  4. document.body.className = 'foo';
  5. document.body.className = 'bar';
  6. document.body.className = 'baz';
  7. let a = observer.takeRecords()[0].attributeName