MutationObserver 监听DOM树变化

2,944 阅读13分钟
原文链接: zhuanlan.zhihu.com

1 概述

Mutation observer 是用于代替 Mutation events 作为观察DOM树结构发生变化时,做出相应处理的API。为什么要使用mutation observer 去代替 mutation events 呢,我们先了解一下mutation events

Mutation Events

Mutation events 是在 DOM3中定义,用于监听DOM树结构变化的事件

它简单的用法如下:

document.getElementById('list').addEventListener("DOMSubtreeModified", function(){
  console.log('列表中子元素被修改');
}, false);

Mutation 事件列表

DOMAttrModified
DOMAttributeNameChanged
DOMCharacterDataModified
DOMElementNameChanged
DOMNodeInserted
DOMNodeRemoved
DOMNodeInsertedIntoDocument
DOMSubtreeModified

其中DOMNodeRemoved,DOMNodeInserted 和 DOMSubtreeModified 分别用于 监听元素子项的删除,新增,修改(包括删除和新增),DOMAttrModified 是监听元素属性的修改,并且能够提供具体的修改动作。

Mutation Events遇到的问题

  • 浏览器兼容性问题
    IE9不支持Mutation Events
    Webkit内核不支持DOMAttrModified特性,
    DOMElementNameChanged和DOMAttributeNameChanged 在Firefox上不被支持。
  • 性能问题
    1.Mutation Events是同步执行的,它的每次调用,都需要从事件队列中取出事件,执行,然后事件队列中移除,期间需要移动队列元素。如果事件触发的较为频繁的话,每一次都需要执行上面的这些步骤,那么浏览器会被拖慢。
    2.Mutation Events本身是事件,所以捕获是采用的是事件冒泡的形式,如果冒泡捕获期间又触发了其他的MutationEvents的话,很有可能就会导致阻塞Javascript线程,甚至导致浏览器崩溃。

Mutation Observer

Mutation Observer 是在DOM4中定义的,用于替代 mutation events 的新API,它的不同于events的是,所有监听操作以及相应处理都是在其他脚本执行完成之后异步执行的,并且是所以变动触发之后,将变得记录在数组中,统一进行回调的,也就是说,当你使用observer监听多个DOM变化时,并且这若干个DOM发生了变化,那么observer会将变化记录到变化数组中,等待一起都结束了,然后一次性的从变化数组中执行其对应的回调函数。

Mutation Observer有以下特点:

  • 它等待所有脚本任务完成后,才会运行,即采用异步方式
  • 它把DOM变动记录封装成一个数组进行处理,而不是一条条地个别处理DOM变动。
  • 它即可以观察发生在DOM节点的所有变动,也可以观察某一类变动

目前,Firefox(14+)、Chrome(26+)、Opera(15+)、IE(11+)和Safari(6.1+)支持这个API。Safari 6.0和Chrome 18-25使用这个API的时候,需要加上WebKit前缀(WebKitMutationObserver)。可以使用下面的表达式检查浏览器是否支持这个API。

var MutationObserver = window.MutationObserver ||
    window.WebKitMutationObserver ||
    window.MozMutationObserver;
// 监测浏览器是否支持
var observeMutationSupport = !!MutationObserver;

使用方法

首先,使用MutationObserver构造函数,新建一个实例,同时指定这个实例的回调函数。

var observer = new MutationObserver(callback);

2.1 observer方法

observer方法指定所要观察的DOM元素,以及要观察的特定变动。

var article = document.querySelector('article');

var options = {
    'childList': true,
    'arrtibutes': true
};

observer.observer(article, options);

上面代码首先指定,所要观察的DOM元素提article,然后指定所要观察的变动是子元素的变动和属性变动。最后,将这两个限定条件作为参数,传入observer对象的observer方法。

MutationObserver所观察的DOM变动(即上面代码的option对象),包含以下类型:

  • childList:子元素的变动
  • attributes:属性的变动
  • characterData:节点内容或节点文本的变动
  • subtree:所有下属节点(包括子节点和子节点的子节点)的变动

想要观察哪一种变动类型,就在option对象中指定它的值为true。需要注意的是,不能单独观察subtree变动,必须同时指定childList、attributes和characterData中的一种或多种。

除了变动类型,option对象还可以设定以下属性:

  • attributeOldValue:值为true或者为false。如果为true,则表示需要记录变动前的属性值。
  • characterDataOldValue:值为true或者为false。如果为true,则表示需要记录变动前的数据值。
  • attributesFilter:值为一个数组,表示需要观察的特定属性(比如['class', 'str'])。

disconnect方法和takeRecord方法

disconnect方法用来停止观察。发生相应变动时,不再调用回调函数。

observer.disconnect();

takeRecord方法用来清除变动记录,即不再处理未处理的变动。

observer.takeRecord

MutationRecord对象

DOM对象每次发生变化,就会生成一条变动记录。这个变动记录对应一个MutationRecord对象,该对象包含了与变动相关的所有信息。Mutation Observer进行处理的一个个变动对象所组成的数组。

MutationRecord对象包含了DOM的相关信息,有如下属性:

  • type:观察的变动类型(attribute、characterData或者childList)。
  • target:发生变动的DOM对象。
  • addedNodes:新增的DOM对象。
  • removeNodes:删除的DOM对象。
  • previousSibling:前一个同级的DOM对象,如果没有则返回null。
  • nextSibling:下一个同级的DOM对象,如果没有就返回null。
  • attributeName:发生变动的属性。如果设置了attributeFilter,则只返回预先指定的属性。
  • oldValue:变动前的值。这个属性只对attribute和characterData变动有效,如果发生childList变动,则返回null。

Mutation Observer 的浏览器兼容范围

2 方法

构造函数

用来实例化一个Mutation观察者对象,其中的参数是一个回调函数,它是会在指定的DOM节点发送变化后,执行的函数,并且会被传入两个参数,一个是变化记录数组(MutationRecord),另一个是观察者对象本身

new MutationObserver(function(records, itself){});

observe

在观察者对象上,注册需要观察的DOM节点,以及相应的参数

void observe(Node target, optional MutationObserverInit options)

其中的可选参数 MutationObserverInit的属性如下:

childLIst 观察目标节点的子节点的新增和删除。
attributes 观察目标节点的属性节点(新增或删除了某个属性,以及某个属性的属性值发生了变化)。
characterData 如果目标节点为characterData节点(一种抽象接口,具体可以为文本节点,注释节点,以及处理指令节点)时,也要观察该节点的文本内容是否发生变化
subtree 观察目标节点的所有后代节点(观察目标节点所包含的整棵DOM树上的上述三种节点变化)
attributeOldValue 在attributes属性已经设为true的前提下, 将发生变化的属性节点之前的属性值记录下来(记录到下面MutationRecord对象的oldValue属性中)
characterDataOldValue 在characterData属性已经设为true的前提下,将发生变化characterData节点之前的文本内容记录下来(记录到下面MutationRecord对象的oldValue属性中)
attributeFilter 一个属性名数组(不需要指定命名空间),只有该数组中包含的属性名发生变化时才会被观察到,其他名称的属性发生变化后会被忽略想要设置那些删选参数的话,

如果想要使用哪个参数的话,就将其值设定为true

disconnect

暂定在观察者对象上设置的节点的变化监听,直到重新调用observe方法

takeRecords

在观察者对象上调用takeRecords 会返回 其观察节点上的变化记录(MutationRecord)数组
其中MutationRecord数组也会作为,观察者初始化时的回调函数的第一个参数
其包含的属性如下:

type 如果是属性发生变化,则返回attributes.如果是一个CharacterData节点发生变化,则返回characterData,如果是目标节点的某个子节点发生了变化,则返回childList.
target 返回此次变化影响到的节点,具体返回那种节点类型是根据type值的不同而不同的,如果type为attributes,则返回发生变化的属性节点所在的元素节点,如果type值为characterData,则返回发生变化的这个characterData节点.如果type为childList,则返回发生变化的子节点的父节点.
addedNodes 返回被添加的节点
removedNodes 返回被删除的节点
previousSibling 返回被添加或被删除的节点的前一个兄弟节点
nextSibling 返回被添加或被删除的节点的后一个兄弟节点
attributeName 返回变更属性的本地名称
oldValue 根据type值的不同,返回的值也会不同.如果type为attributes,则返回该属性变化之前的属性值.如果type为characterData,则返回该节点变化之前的文本数据.如果type为childList,则返回null
3 使用实例
// Firefox和Chrome早期版本中带有前缀
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver
// 选择目标节点
var target = document.querySelector('#some-id'); 
// 创建观察者对象
var observer = new MutationObserver(function(mutations) {  
  mutations.forEach(function(mutation) { 
    console.log(mutation.type); 
  }); 
}); 
// 配置观察选项:
var config = { attributes: true, childList: true, characterData: true } 
// 传入目标节点和观察选项
observer.observe(target, config); 
// 随后,你还可以停止观察
observer.disconnect();

子元素的变动

下面的例子说明如果读取变动记录。

<!DOCTYPE html>
<html>

	<head>
		<title>演示Vue</title>
		<style>

		</style>
	</head>

	<body>
		<div id='demo'>
			<ol>
				<li>111111</li>
			</ol>
		</div>
	</body>

	<script type="text/javascript">
		var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
		var list = document.querySelector('ol');

		var Observer = new MutationObserver(function(mutations, instance) {
			console.log(mutations);
			console.log(instance);
			mutations.forEach(function(mutation) {
				console.log(mutation);
			});
		});

		Observer.observe(list, {
			childList: true,
			subtree: true
		});
		list.appendChild(document.createElement('div'));
		list.appendChild(document.createTextNode('foo'));
		list.removeChild(list.childNodes[0]);
		list.childNodes[0].appendChild(document.createElement('div'));
	</script>

</html>

上面代码的观察器,观察body元素的所有下级元素(childList表示观察子元素,subtree表示观察子元素的下级元素)的变动。回调函数会在控制台显示所有变动的类型和目标元素。

监测characterData的变动

下面的例子说明如何追踪属性的变动。

<!DOCTYPE html>
<html>

	<head>
		<title>演示Vue</title>
		<style>

		</style>
	</head>

	<body>
		<div id='demo'>
			<ol>
				<li>111111</li>
			</ol>
		</div>
	</body>

	<script type="text/javascript">
		var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
		var list = document.querySelector('ol');

		var Observer = new MutationObserver(function(mutations, instance) {
			mutations.forEach(function(mutation) {
				console.log(mutation);
			});
		});
		Observer.observe(list, {
			childList: true,
			characterData: true,
			subtree: true
		});
		list.childNodes[0].data = "cha";
	</script>

</html>

上面代码先设定追踪属性变动('attributes': true),然后设定记录变动前的值。实际发生变动时,会将变动前的值显示在控制台。

监测属性的变动

<!DOCTYPE html>
<html>

	<head>
		<title>演示Vue</title>
		<style>

		</style>
	</head>

	<body>
		<div id='demo'>
			<ol>
				<li>111111</li>
			</ol>
		</div>
	</body>

	<script type="text/javascript">
		var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
		var list = document.querySelector('ol');

		var Observer = new MutationObserver(function(mutations, instance) {
			mutations.forEach(function(mutation) {
				console.log(mutation);
			});
		});
		Observer.observe(list, {
			attributes: true
		});
		// 设置节点的属性  会触发回调函数
		list.setAttribute('data-value', '111');

		// 重新设置属性 会触发回调
		list.setAttribute('data-value', '2222');

		// 删除属性 也会触发回调
		list.removeAttribute('data-value');
	</script>

</html>

除了基本的变动类型之外,options对象还可以设定以下属性
attributeOldValue: {boolean} 表示观察attributes变动时,是否需要记录变动前的属性值。
characterDataOldValue: {boolean} 表示观察characterData变动时,是否需要记录变动前的值。
attributeFilter {Array} 表示需要观察的特定属性 比如 ['class', 'src']

1. attributeOldValue 属性变动前,是否需要记录变动之前的值; 代码如下:

<!DOCTYPE html>
<html>
  <head>
    <title>演示Vue</title>
    <style>
      
    </style>
  </head>
  <body>
    <div id='demo'>
      <ol>
        <li>111111</li>
      </ol>
    </div>
  </body>

  <script type="text/javascript">
     var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
     var list = document.querySelector('ol');

     var Observer = new MutationObserver(function(mutations, instance){
        mutations.forEach(function(mutation){
          console.log(mutation);
        });
     });
    Observer.observe(list, {
      attributes: true,
      attributeOldValue: true  
    });   
    // 设置节点的属性  会触发回调函数
    list.setAttribute('data-value', '111');

    // 删除属性 
    list.setAttribute('data-value', '2222');
  </script>
</html>

2.characterData变动时,是否需要记录变动前的值。

<!DOCTYPE html>
<html>
  <head>
    <title>演示Vue</title>
    <style>
      
    </style>
  </head>
  <body>
    <div id='demo'>
      <ol>
        <li>111111</li>
      </ol>
    </div>
  </body>

  <script type="text/javascript">
     var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
     var list = document.querySelector('ol');

     var Observer = new MutationObserver(function(mutations, instance){
        mutations.forEach(function(mutation){
          console.log(mutation);
        });
     });
    Observer.observe(list, {
      childList:true,
      characterData:true,
      subtree:true,
      characterDataOldValue: true
    });   
    // 设置数据 触发回调
    list.childNodes[0].data = "aaa";

    // 重新设置数据 重新触发回调
    list.childNodes[0].data = "bbbb";
  </script>
</html>

第一次设置数据,记录变动前的数据如下:

第二次设置数据,记录变动前的回调如下:

可以看到oldValue 的变动值;

attributeFilter {Array} 表示需要观察的特定属性 比如 ['class', 'src'];代码如下:

<!DOCTYPE html>
<html>
  <head>
    <title>演示Vue</title>
    <style>
      
    </style>
  </head>
  <body>
    <div id='demo'>
      <ol>
        <li>111111</li>
      </ol>
    </div>
  </body>

  <script type="text/javascript">
     var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
     var list = document.querySelector('ol');

     var Observer = new MutationObserver(function(mutations, instance){
        mutations.forEach(function(mutation){
          console.log(mutation);
        });
     });
    Observer.observe(list, {
      attributes: true,
      attributeFilter: ['data-value']
    });   
    // 第一次设置属性 data-key 不会触发的,因为data-value 不存在
    list.setAttribute('data-key', 1);

    // 第二次会触发
    list.setAttribute('data-value', 1);
  </script>
</html>

下面我们做一个简单的demo编辑器,首先给父级元素 ol 设置 contenteditable 让容器可编辑,然后构造一个observer 监听子元素的变化,每次回车的时候,控制台输出它的内容;如下代码:

<!DOCTYPE html>
<html>
  <head>
    <title>演示Vue</title>
    <style>
      
    </style>
  </head>
  <body>
    <div id='demo'>
      <ol contenteditable oninput="" style='border: 1px solid red'>
        <li>111111</li>
      </ol>
    </div>
  </body>

  <script type="text/javascript">
     var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
     var list = document.querySelector('ol');

     var Observer = new MutationObserver(function(mutations, instance){
        mutations.forEach(function(mutation){
          console.log(mutation);
          if(mutation.type === 'childList') {
            var list_values = [].slice.call(list.children).map(function(node) {
              return node.innerHTML;
            }).filter(function(s) {
              if(s === '<br>') {
                return false;
              } else {
                return true;
              }
            });
            console.log(list_values);
          }
        });
     });
    Observer.observe(list, {
      childList: true
    });   
    
  </script>
</html>

现在我们继续可以做一个类似于 input和textarea中的 valueChange的事件一样的,监听值变化,之前的值和之后的值,如下代码:

<!DOCTYPE html>
<html>
  <head>
    <title>演示Vue</title>
    <style>
      
    </style>
  </head>
  <body>
    <div id='demo'>
      <ol contenteditable oninput="" style='border: 1px solid red'>
        <li>111111</li>
      </ol>
    </div>
  </body>

  <script type="text/javascript">
     var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
     var list = document.querySelector('ol');

     var Observer = new MutationObserver(function(mutations, instance){
        mutations.forEach(function(mutation){
          var enter = {
            mutation: mutation,
            el: mutation.target,
            newValue: mutation.target.textContent,
            oldValue: mutation.oldValue
          };
          console.log(enter);
        })
     });
    Observer.observe(list, {
      childList: true,
      attributes: true,
      characterData: true,
      subtree: true,
      characterDataOldValue: true,
    });   
    
  </script>
</html>

注意: 对input 和 textarea 不起作用的。

编辑器统计字数的demo:

<!DOCTYPE html>
<html>
  <head>
    <title>演示Vue</title>
    <style>
      
    </style>
  </head>
  <body>
    <div id='editor' contenteditable style="width: 240px; height: 80px; border: 1px solid red;"></div>
    <p id="textInput">还可以输入100字</p>
  </body>

  <script type="text/javascript">
     var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
     var editor = document.querySelector('#editor');
     var textInput = document.querySelector('#textInput');
     var observer = new MutationObserver(function(mutations){
        mutations.forEach(function(mutation) {
          if(mutation.type === 'characterData') {
            var newValue = mutation.target.textContent;
            textInput.innerHTML = "还可以输入" +  (1000 - newValue.length+"字");
          }
        });
     });
    observer.observe(editor, {
      childList: true,
      attributes: true,
      characterData: true,
      subtree: true,
      characterDataOldValue: true,
    });   
    
  </script>
</html>