DOM 规范中的 MutationObserver 接口,可以在 DOM 被修改时异步执行回调。使 用 MutationObserver 可以观察整个文档、DOM 树的一部分,或某个元素。此外还可以观察元素属性、子节点、文本,或者前三者任意组合的变化。
MutationObserver接口是为了取代废弃的MutationEvent。
###一、基本用法 MutationObserver 的实例要通过调用 MutationObserver 构造函数并传入一个回调函数来创建:
let observer = new MutationObserver(() => console.log('DOM was mutated!'));
- observe()方法
新创建的 MutationObserver 实例不会关联 DOM 的任何部分。要把这个 observer 与 DOM 关 联起来,需要使用 observe()方法。这个方法接收两个必需的参数:要观察其变化的 DOM 节点,以及 一个 MutationObserverInit 对象。
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
执行以上代码后,元素上任何属性发生变化都会被这个 MutationObserver 实例发现,然 后就会异步执行注册的回调函数。
注意,回调中的 console.log()是后执行的。这表明回调并非与实际的 DOM 变化同步执行。
- 回调与 MutationRecord
每个回调都会收到一个 MutationRecord 实例的数组。MutationRecord 实例包含的信息包括发 生了什么变化,以及 DOM 的哪一部分受到了影响。因为回调执行之前可能同时发生多个满足观察条件 的事件,所以每次执行回调都会传入一个包含按顺序入队的 MutationRecord 实例的数组。 下面展示了反映一个属性变化的 MutationRecord 实例的数组:
let observer = new MutationObserver((mutationRecords) => console.log(mutationRecords));
observer.observe(document.body, { attributes: true });
document.body.setAttribute('foo', 'bar');
连续修改会生成多个 MutationRecord 实例,下次回调执行时就会收到包含所有这些实例的数组, 顺序为变化事件发生的顺序:
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]
- disconnect()方法
默认情况下,只要被观察的元素不被垃圾回收,MutationObserver 的回调就会响应 DOM 变化事 件,从而被执行。要提前终止执行回调,可以调用 disconnect()方法。下面的例子演示了同步调用 disconnect()之后,不仅会停止此后变化事件的回调,也会抛弃已经加入任务队列要异步执行的回调:
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():
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
- 复用 MutationObserver
多次调用 observe()方法,可以复用一个 MutationObserver 对象观察多个不同的目标节点。此 时,MutationRecord 的 target 属性可以标识发生变化事件的目标节点。下面的示例演示了这个过程:
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>]
//disconnect()方法是一个“一刀切”的方案,调用它会停止观察所有目标:
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');
// (没有日志输出)
- 重用 MutationObserver
调用 disconnect()并不会结束 MutationObserver 的生命。还可以重新使用这个观察者,再将 它关联到新的目标节点。下面的示例在两个连续的异步块中先断开然后又恢复了观察者与元素 的关联:
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
###二、观察对象 观察者可以观察的事 件包括属性变化、文本变化和子节点变化。 1.subtree(观察子树)
布尔值,表示除了目标节点,是否观察目标节点的子树(后代) 如果是 false,则只观察目标节点的变化;如果是 true,则观察目标节点及其整个子树 11 默认为 false
// 清空主体
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",
// }
// ]
2.attributes(观察属性)
布尔值,表示是否观察目标节点的属性变化 默认为 false
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]
3.attributeFilter(观察白名单属性)
字符串数组,表示要观察哪些属性的变化 把这个值设置为 true 也会将 attributes 的值转换为 true 默认为观察所有属性
把 attributes 设置为 true 的默认行为是观察所有属性,但不会在 MutationRecord 对象中记 录原来的属性值。如果想观察某个或某几个属性,可以使用 attributeFilter 属性来设置白名单,即 一个属性名字符串数组:
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]
4.attributeOldValue(观察历史属性)
布尔值,表示 MutationRecord 是否记录变化之前的属性值 把这个值设置为 true 也会将 attributes 的值转换为 true 默认为 false
如果想在变化记录中保存属性原来的值,可以将 attributeOldValue 属性设置为 true:
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']
5.characterData(观察字符数据)
布尔值,表示修改字符数据是否触发变化事件 默认为 false
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]
6.characterDataOldValue(观察变化之前字符数据)
布尔值,表示 MutationRecord 是否记录变化之前的字符数据 把这个值设置为 true 也会将 characterData 的值转换为 true 默认为 false
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"]
7.childList(观察子节点)
布尔值,表示修改目标节点的子节点是否触发变化事件 默认为 false
下面的例子演示了添加子节点:
// 清空主体
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",
// }
// ]
下面的例子演示了移除子节点:
// 清空主体
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",
// }
// ]
对子节点重新排序(尽管调用一个方法即可实现)会报告两次变化事件,因为从技术上会涉及先移 除和再添加:
// 清空主体
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",
// }
// ]
#####注意 在调用observe()时,MutationObserverInit对象中的attribute、characterData 和childList属性必须至少有一项为true(无论是直接设置这几个属性,还是通过设置 attributeOldValue 等属性间接导致它们的值转换为 true)。否则会抛出错误,因为没 有任何变化事件可能触发回调。
###三、异步回调与记录队列 MutationObserver 接口是出于性能考虑而设计的,其核心是异步回调与记录队列模型。为了在 大量变化事件发生时不影响性能,每次变化的信息(由观察者实例决定)会保存在 MutationRecord 实例中,然后添加到记录队列。这个队列对每个 MutationObserver 实例都是唯一的,是所有 DOM 变化事件的有序列表。
- 记录队列
调用 MutationObserver 实例的 takeRecords()方法可以清空记录队列,取出并返回其中的所 4 有 MutationRecord 实例。看这个例子: 每次 MutationRecord 被添加到 MutationObserver 的记录队列时,仅当之前没有已排期的微任 务回调时(队列中微任务长度为 0),才会将观察者注册的回调(在初始化 MutationObserver 时传入) 作为微任务调度到任务队列上。这样可以保证记录队列的内容不会被回调处理两次。 不过在回调的微任务异步执行期间,有可能又会发生更多变化事件。因此被调用的回调会接收到一 个 MutationRecord 实例的数组,顺序为它们进入记录队列的顺序。回调要负责处理这个数组的每一 个实例,因为函数退出之后这些实现就不存在了。回调执行后,这些 MutationRecord 就用不着了, 因此记录队列会被清空,其内容会被丢弃。 2. takeRecords()方法
调用 MutationObserver 实例的 takeRecords()方法可以清空记录队列,取出并返回其中的所 4 有 MutationRecord 实例。看这个例子:
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]
// []
这在希望断开与观察目标的联系,但又希望处理由于调用 disconnect()而被抛弃的记录队列中的 MutationRecord 实例时比较有用。
###四、性能、内存与垃圾回收 DOM Level 2 规范中描述的 MutationEvent 定义了一组会在各种 DOM 变化时触发的事件。由于 浏览器事件的实现机制,这个接口出现了严重的性能问题。因此,DOM Level 3 规定废弃了这些事件。 10 MutationObserver 接口就是为替代这些事件而设计的更实用、性能更好的方案。 将变化回调委托给微任务来执行可以保证事件同步触发,同时避免随之而来的混乱。为 Mutation- Observer 而实现的记录队列,可以保证即使变化事件被爆发式地触发,也不会显著地拖慢浏览器。 无论如何,使用 MutationObserver 仍然不是没有代价的。因此理解什么时候避免出现这种情况 就很重要了。
- MutationObserver 的引用
MutationObserver 实例与目标节点之间的引用关系是非对称的。MutationObserver 拥有对要观察的目标节点的弱引用。因为是弱引用,所以不会妨碍垃圾回收程序回收目标节点。
然而,目标节点却拥有对 MutationObserver 的强引用。如果目标节点从 DOM 中被移除,随后被垃圾回收,则关联的 MutationObserver 也会被垃圾回收。 2. MutationRecord 的引用
记录队列中的每个 MutationRecord 实例至少包含对已有 DOM 节点的一个引用。如果变化是 childList 类型,则会包含多个节点的引用。记录队列和回调处理的默认行为是耗尽这个队列,处理 每个 MutationRecord,然后让它们超出作用域并被垃圾回收。
有时候可能需要保存某个观察者的完整变化记录。保存这些 MutationRecord 实例,也就会保存 它们引用的节点,因而会妨碍这些节点被回收。如果需要尽快地释放内存,建议从每个 MutationRecord 中抽取出最有用的信息,然后保存到一个新对象中,最后抛弃 MutationRecord。