边学边译JS工作机制--10. 使用MutationObserver 跟踪 DOM 变化

831 阅读4分钟

本系列其他译文请看JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn) 本章阅读指数:3
本章内容,适合做一些性能检测或者前端测试方案,可根据自身情况阅读

客户端的Web应用已经越来越重了,这是由于人民群众日益增长的审美需求导致的。客户端需要提供更丰富的交互,实时计算等等。 复杂性的增长,让我们准确的知道运行期间任意时刻UI的状态、 当你构建一个框架或者库,需要通过监测 DOM 来响应并执行一些特定的操作,那就更困难了。

概览

MutationObserver 是现代浏览器提供的WEB API,用来监听DOM的变化。用它可以监听到最新增加/删除的节点,节点属性的改变,以及文本节点中文本的改变。

什么场景下要这么做呢?

  • 你想通知用户,他当前的页面已经改变了。
  • 使用一些根据DOM变化动态加载JS模块的框架
  • 如果是一个WYSIWYG编辑器,要实现undo/redo功能。通过MutationObserver API,你就知道发生了什么改变,然后轻松的undo它们。

image.png 看看MutationObserver 是如何起作用的。

怎么使用MutationObserver

在应用中实现MutationObserver是非常简单的。你可以在创建MutationObserver 实例时传递一个函数,这个函数会在每次DOM改变时触发。这个函数的第一个参数,是在一个批次中所有发生变化的DOM集合。每一个改变,包含了它的类型和发生的改变。

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

创建的对象有三个方法

  • observe — 开始监听变化。有两个参数,你想监听的DOM和一个配置的对象。
  • disconnect — 停止监听
  • takeRecords — 在回调触犯之前返回最近一个批次的变换,

启动监听:

// Starts listening for changes in the root HTML element of the page.
mutationObserver.observe(document.documentElement, {
  attributes: true,
  characterData: true,
  childList: true,
  subtree: true,
  attributeOldValue: true,
  characterDataOldValue: true
});

看一个简单的例子

<div id="sample-div" class="test"> Simple div </div>

使用jQuery 删除class 属性

$("#sample-div").removeAttr("class");

调用mutationObserver.observe(...) 之后,我们将会看到打印出MutationRecord日志 image.png 这次的变化是通过移除 class 属性触发的.

当任务完成之后,如果要停止监听DOM,要这么做

// Stops the MutationObserver from listening for changes.
mutationObserver.disconnect();

如今 MutationObserver 已经被广泛支持了:

image.png

替代方案

但是MutationObserver还没有完全推开,在此之前开发者需要怎么做呢

有一些其他的可选方案:

  • Polling
  • MutationEvents
  • CSS animations

Polling (轮询)

最简单暴力的方式是去轮询。使用浏览器的setInterval方法,可以设置一个任务,周期性的检查有什么变化。这个方式自然会影响到app的性能。

MutationEvents

早在2000年,就引入了MutationEvents API 。尽管很有用,但是DOM的每一次改变都会触发mutation事件,这也引入了性能问题。如今MuatationAPI已经被遗弃了,很快所有的浏览器也都会不支持。 MutationEvents支持情况:

image.png

CSS 动画

一个相对陌生的替代方案是,是使用CSS animations. 听起来有点诡异。这个方案是创建一个动画,一旦DOM中添加元素就会被触发。动画一开始, animationstart 事件就会被触发:如果此时你添加了对该事件的监听,你就会知道DOM中添加了什么元素。动效的执行时间应该很小,这样用户觉察不到。

首先,我们需要一个父元素包含它,然后插入节点监听

<div id=”container-element”></div>

为了监听节点插入,我们需要设置一系列的关键帧,该动画在添加节点的时候启动:

@keyframes nodeInserted { 
 from { opacity: 0.99; }
 to { opacity: 1; } 
}

关键帧创建之后,动画需要应用在你想监听的元素上。注意执行时间要断--这样浏览器中几乎感受不到

#container-element * {
 animation-duration: 0.001s;
 animation-name: nodeInserted;
}

这个代码把动画添加到container-element的所有子节点上,当动画结束,会触发插入事件

我们需要一个JS函数,作为事件的监听者。在函数内部,初始化event.animationName检查必须确保是我们所监听的动画

var insertionListener = function(event) {
  // Making sure that this is the animation we want.
  if (event.animationName === "nodeInserted") {
    console.log("Node has been inserted: " + event.target);
  }
}

给父节点添加监听:

document.addEventListener(“animationstart”, insertionListener, false); // standard + firefox
document.addEventListener(“MSAnimationStart”, insertionListener, false); // IE
document.addEventListener(“webkitAnimationStart”, insertionListener, false); // Chrome + Safari

CSS动画的支持情况:

image.png

MutationObserver的提供了比上述都好的方案。它包含可每一个DOM中可能发生的变化,而且性能更好。最关键的是,几乎所有的浏览器都支持它,同时还有两个基于MutationEvents实现的polyfills