MutationObserver DOM监听

背景

什么情况下会需要监听DOM?高度解耦DOM的情况下,比如滚动加载来判断元素是否已经加入父元素;监听js UI框架引起的DOM修改;记录DOM操作情况等。。。

MutationObserver接口提供了监视对DOM树所做更改的能力。它被设计为旧的Mutation Events功能的替代品,该功能是DOM3 Events规范的一部分。Vue的源码里你也能找到对它的使用

Mutation Events

Mutation Events总共有7种事件:

  • DOMNodeInserted
  • DOMNodeRemoved
  • DOMSubtreeModified
  • DOMAttrModified
  • DOMCharacterDataModified
  • DOMNodeInsertedIntoDocument
  • DOMNodeRemovedFromDocument。

Mutation Events的兼容情况:

  • Mutation Events在IE浏览器中最低支持到IE9
  • 在webkit内核的浏览器中,不支持DOMAttrModified事件
  • IE,Edge以及Firefox浏览器下不支持DOMNodeInsertedIntoDocument和DOMNodeRemovedFromDocument事件

Mutation Events中的所有事件都被设计成无法取消,如果可以取消Mutation Events事件则会导致现有的DOM接口无法对文档进行改变,比如appendChild,remove等添加和删除节点的DOM操作。

兼容

c-mutationevent.png

兼容还行,主要运用于IE9/10。

存在问题

Mutation Events中最令人诟病的就是性能以及安全性的问题。如:

1
2
3
4
document.addEventListener('DOMNodeInserted', function() {
let newElement = document.createElement('div');
document.body.appendChild(newElement);
});

document下的所有DOM添加操作都会触发DOMNodeInserted方法,这时就会出现无限循环调用DOMNodeInserted方法,导致浏览器崩溃。

还有就是Mutation Events是事件机制,因此会有一般事件都存在的捕获和冒泡阶段,此时如果在捕获和冒泡阶段又对DOM进行了操作会拖慢浏览器的运行。

另一点就是Mutation Events事件机制是同步的,也就是说每次DOM修改就会触发,修改几次就触发几次,严重降低浏览器的运行,严重时甚至导致线程崩溃

有关Mutation Events的更多详细介绍可见mdn-Mutation_events

MutationObserver

作为Mutation Events功能的替代品,MutationObserver弥补了以上Mutation Events的问题。

基本使用

构造函数MutationObserver

语法

1
var observer = new MutationObserver(callback);

其中参数:

  • callback:一个回调函数,每当被指定的节点或子树以及配置项有Dom变动时会被调用。回调函数拥有两个参数:
    • 一个是描述所有被触发改动的MutationRecord对象数组
    • 另一个是调用该函数的MutationObserver 对象。

返回值:

  • 一个新的、包含监听Dom变化回调函数的MutationObserver 对象

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div id="testdom"></div>

<script>
let $testEle = document.querySelector('#testdom');
let observerOptions = {
childList: true, // 观察目标子节点变换
attributes: true, // 观察属性变动
subtree: true, // 观察后代节点
};

let observer = new MutationObserver(function (mutationList, observer) {
console.log(mutationList, observer)
});

observer.observe($testEle, observerOptions); // 当观察者 observer 发现更改匹配到观察请求中指定的配置项时,callback方法将会被调用;调用 observe() 开始观察 DOM。

// test
$testEle.appendChild((function () {
let $newEle = document.createElement('p');
p.innerText = 'test';
return p;
} ())
</script>

观察者实例方法

observe()设置观察目标

observe()方法配置了MutationObserver对象的回调方法以开始接收与给定选项匹配的DOM变化的通知。 根据配置,这个观察者会观察DOM树中的单个Node,或者指定的节点及其某些或者所有的子孙节点。

语法:

1
mutationObserver.observe(target[, options])

其中参数:

  • target:DOM树中的一个要观察变化的DOM Node (可能是一个Element) , 或者是被观察的子节点树的根节点。
  • options:(可选)一个可选的MutationObserverInit 对象,此对象的配置项描述了DOM的哪些变化应该提供给当前观察者的callback。具体MutationObserverInit对象字段如下
    • attributeFilter:(可选)要监视的特定属性名称的数组。如果未包含此属性,则对所有属性的更改都会触发变动通知。无默认值。
    • attributeOldValue:(可选)当监视节点的属性改动时,将此属性设为true 将记录任何有改动的属性的上一个值。有关观察属性更改和值记录的详细信息,详见Monitoring attribute values in MutationObserver。无默认值。
    • attributes:(可选)设为true以观察受监视元素的属性值变更。默认值为false。
    • characterData:(可选)设为true以监视指定目标节点或子节点树中节点所包含的字符数据的变化。无默认值。
    • characterDataOldValue:(可选)设为true以在文本在受监视节点上发生更改时记录节点文本的先前值。无默认值。
    • childList:(可选)设为true以监视目标节点(如果subtree为true,则包含子孙节点)添加或删除新的子节点。默认值为false。
    • subtree:(可选) 设为true以扩展监视范围到目标节点下的整个子树的所有节点。MutationObserverInit的其他值都会作用于此子树下的所有节点,而不仅仅只作用于目标节点。默认值为false。

*以下任一情况都会抛出异常:

  • 配置选项使得实际上不会监视任何内容(例如,如果MutationObserverInit.childList,MutationObserverInit.attributes和MutationObserverInit.characterData都为false)。
  • attributes 选项为false (表示不监视属性更改)但是attributeOldValue 为 true 并且/或者 attributeFilter 配置存在。
  • The characterDataOldValue选项为 true 但是MutationObserverInit.characterData 为 false (表示不跟踪字符更改)。
disconnect()阻止观察者观察任何改变

disconnect() 方法告诉观察者停止观察变动。 可以通过调用其observe()方法来重用观察者。

如上例:

1
observer.disconnect()

如果被观察的元素被从DOM中移除,然后被浏览器的垃圾回收机制释放,此MutationObserver将同样被删除。

takeRecords()清空记录队列并返回里面的内容

takeRecords() 方法返回已检测到但尚未由观察者的回调函数处理的所有匹配DOM更改的列表,使变更队列保持为空。 此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改。

语法:

1
mutationRecords = mutationObserver.takeRecords()

返回值:

  • 返回一个MutationRecord 对象列表,每个对象都描述了应用于DOM树某部分的一次改动。

MutationRecord 对象

属性 类型 描述
MutationRecord.type String 如果是一个属性变化,则返回 “attributes”;如果是一个 characterData 节点的变化,则返回 “characterData”;如果是子节点树变化,则返回 “childList”。
MutationRecord.target Node 根据 MutationRecord.type 返回变化所影响的节点。对于属性变化(MutationRecord.type === “attributes”),返回属性变化的节点。对于 characterData 变化(MutationRecord.type === “characterData”),返回 characterData 节点。对于子节点树变化(MutationRecord.type === “childList”),返回子节点变化的节点。
MutationRecord.addedNodes NodeList 返回添加的节点。如果没有节点被添加,则该属性将是一个空的 NodeList
MutationRecord.removedNodes NodeList 返回移除的节点。如果没有节点被移除,则该属性将是一个空的 NodeList
MutationRecord.previousSibling Node 返回被添加或移除节点之前的兄弟节点,或 null。
MutationRecord.nextSibling Node 返回被添加或移除节点之后的兄弟节点,或 null。
MutationRecord.attributeName String 返回被修改属性的属性名,或 null。
MutationRecord.attributeNamespace String 返回被修改属性的命名空间,或 null。
MutationRecord.oldValue String 返回值取决于 MutationRecord.type。对于属性变化,返回变化之前的属性值;对于 characterData,返回变化之前的 data;对于子节点树变化,返回 null。

MutationObserver选项与MutationEvent名称之间的对应关系:

MutationEvent MutationObserver options
DOMNodeInserted { childList: true, subtree: true }
DOMNodeRemoved { childList: true, subtree: true }
DOMSubtreeModified { childList: true, subtree: true }
DOMAttrModified { attributes: true, subtree: true }
DOMCharacterDataModified { characterData: true, subtree: true }

兼容

c-mutationobserver.png

兼容情况良好

不支持Mutation Events或MutationObserver的情况如何处理呢?比方万一出现需求一定要在IE6/7下实现

CSS3 animation来实现DOM监听

利用动画的DOM触发事件监听。比如创建一个动画,一旦一个元素被添加到了DOM,这个动画就将会被触发。动画开始时即触发animationstart事件。当然利用这种方式我们要确保动画对展示界面的影响是可以忽略不计的。


相关链接