有时,当节点在DOM树中更改时,需要通知您。 在最近的文章中,我们研究了从约束验证到遍历节点的所有内容。 我们始终假设仅在执行时才知道有关特定节点结构的信息。 这种假设在大多数情况下是正确的,但在视图管理器方面可能有缺陷。
视图管理器试图使视图与某些基础数据保持一致,并且还可能希望使视图在另一个方向上保持一致。 如果视图中的某些内容发生更改,则这些更改需要反映在原始数据集中。
在本教程中,我们将了解MutationObserver
,它为我们提供了一种观察DOM树中的更改的方法。 它旨在替代DOM L3事件规范中定义的原始突变事件。
倾听变化
Mutation Events API是在2000年左右指定的。它应该提供一种简单的方法来观察DOM树中的变化并对变化做出反应。 它由几个不同的事件组成,例如DOMNodeRemoved
和DOMAttrModified
。 节点更改后,事件即直接(即同步)触发。
即使此功能没有得到很大的普及,它还是提供了一种非常方便的观察更改的方法。 浏览器扩展是最流行的用例之一。 如果页面中的某些内容发生更改,则可以通知已安装的扩展。 此时,可以在页面上执行一些工作。
因此,让我们看一个经典的解决方案,用于通知DOM树中的潜在更改。 设置正确的侦听器与以下代码段一样简单。
["DOMNodeInserted", "DOMAttrModified", "DOMNodeRemoved"].forEach(function (eventName) {
document.documentElement.addEventListener(eventName, callback, true);
});
侦听器的定义如下。 我们打开事件参数的attrChange
属性。
var callback = function (ev) {
var nodeOrAttr = ev.relatedNode;
switch (ev.attrChange) {
case MutationEvent.MODIFICATION:
console.log('changed attribute', ev.attrName, ev.prevValue, ev.newValue, nodeOrAttr);
break;
case MutationEvent.ADDITION:
console.log('added attribute', ev.attrName, ev.prevValue, ev.newValue, nodeOrAttr);
break;
case MutationEvent.REMOVAL:
console.log('removed attribute', ev.attrName, ev.prevValue, ev.newValue, nodeOrAttr);
break;
default:
console.log(ev.type, nodeOrAttr);
break;
}
};
上面的代码测试可以在任何HTML页面上执行。 我们使用以下代码查看如何调用我们的侦听器。
var newNode = document.createElement('div');
newNode.setAttribute('id', 'initial');
document.body.appendChild(newNode);
newNode.setAttribute('class', 'foo');
newNode.setAttribute('id', 'bar');
newNode.removeAttribute('id');
document.body.removeChild(newNode);
在Firefox中,我们将看到类似于下图的输出。
不幸的是,在所有浏览器中观察到的行为都不相同。 我们刚刚介绍的方法存在许多细微的问题。 最大的担忧之一是,在大多数流行的浏览器中,尚未以完整且可互操作的方式来实现Mutation事件。
另一个原因是,即使Mutation事件非常有用,但它们始终是导致性能问题的原因。 他们很慢。 部分原因是它们以同步方式过于频繁地发射。 同样,我们可能最终会以递归循环的形式填充堆栈。 最后,它们可能是浏览器中某些不良错误的根源。
我们可以做得更好吗? 我们可以! 现在,我们介绍MutationObserver
接口。
变异观察者
在DOM L4规范中引入的突变观察者将替代以前的突变事件。 有许多主要差异。 对我们来说,最重要的是异步执行。 MutationObserver
实例永远不会在当前事件循环周期中触发,而是排队等待在下一个实例循环中运行。 结果,变异观察者聚集了变化。 我们可能没有收到包含所有更改的单个事件,而是收到了许多更改的许多事件。
异步批处理非常有用,因为每次更改DOM时都不会调用我们的观察器方法。 取而代之的是,在所有更改完成之后,将调用回调(很快)。 这避免了同时执行的损失,因为我们在有机会做出反应之前不需要关注未样式化内容的闪烁。
这是回调查找突变观察者的方式:
var callback = function (mutations) {
mutations.forEach(function (mutation) {
var target = mutation.target;
switch (mutation.type) {
case 'attributes':
var attribute = mutation.attributeName;
var oldValue = mutation.oldValue;
var newValue = target.getAttribute(attribute);
if (mutation.oldValue === null)
console.log('added attribute', attribute, '', newValue, target);
else
console.log('changed attribute', attribute, oldValue, newValue, target);
break;
case 'childList':
if (mutation.addedNodes.length > 0)
console.log('added nodes', mutation.addedNodes, target);
else if (mutation.removedNodes.length > 0)
console.log('removed nodes', mutation.removedNodes, target);
break;
}
});
};
回调将传递给构造函数以创建一个新的MutationObserver
实例。 以下代码段使用了所有可能的选项来开始观察。 实际上,我们可以忽略禁用的选项。
var mo = new MutationObserver(callback);
mo.observe(document.documentElement, {
childList: true,
attributes: true,
characterData: false,
subtree: true,
attributeOldValue: true,
characterDataOldValue: false,
});
显然,使用MutationObserver
有很多事情,但是从本质MutationObserver
,它可以归结为:
- 创建一个带有回调的新
MutationObserver
对象,以处理抛出的所有事件。 - 告诉
MutationObserver
对象观察具有所需选项的特定节点。 - 通过断开
MutationObserver
对象的连接来停止观察事件,即
mo.disconnect();
上面的示例使用Mutation事件对与上一个相同的突变做出了反应。 主要区别在于我们使用MutationObserver
,它为我们提供了更好的性能,支持和灵活性。 在Firefox中,我们现在使用MutationObserver
获得以下信息。
即使结果看起来非常相似,也已经明显看出了一个关键的区别:在所有示例中,目标节点都已经被突变。 让我们看一下added attribute
事件的行。 在这里,我们添加一个新的class=foo
属性。 但是,该属性已经存在。 下一行更加明显。 这是通过将id
属性的值从initial
更改为bar
。 但是,我们没有机会看到新属性,也没有任何id
属性附加到节点。
原因是我们已经(请记住,我们的测试功能是在一个步骤中执行的!)删除了该节点。 MutationObserver
调度的异步执行根本不会干扰我们的代码。 因此,结果可能缺少前一种方法的一些交互可能性,但会增加性能和批处理执行。 这些属性值得更多。
实际例子
我们已经提到过,浏览器扩展是需要利用突变观察者的应用程序的典范。 但是,有些框架也离不开它们。 现在,我们要看两个流行的MVC框架,Aurelia和Polymer。
Aurelia是街区的新孩子之一。 它是用于多个平台的最新客户端框架。 它的功能之一是它更喜欢约定而不是配置。 Aurelia团队面临的挑战之一是如何支持IE9之类的浏览器。 他们确定缺少的MutationObserver
是其兼容性问题的根本原因。 最终,他们决定用polyfill填充该Kong。
在内部,Aurelia在其模板模块中观察DOM树的修改。 核心框架不关心此类更改。 在模板模块中,我们找到了一个名为ChildObserver
的类,该类在ChildObserverBinder
实例中使用MutationObserver
。 儿童观察者将这些粘合剂用于每个目标或行为观察。 在资料夹中,我们看到与以下代码段相似的代码。
function bind (source) {
this.observer.observe(this.target, { childList:true, subtree: true });
var results = this.target.querySelectorAll(this.selector);
for (var i = 0; i < results.length; ++i)
this.behavior[this.property].push(results[i]);
}
function unbind () {
this.observer.disconnect();
}
在构造函数中创建新的MutationObserver
实例后,我们可以使用bind
方法从源头实际观察基本target
上的更改。 尽管当前不同的源均无效,但已记录观察到的行为。 主要机制可以在上面的代码中找到。 我们获得满足特定选择器的所有节点,并将它们添加到具有指定属性名称的行为列表中。 然后,在发生突变通知的情况下,将使用此列表来确定这些突变是否有趣。
Polymer是广泛使用MutationObserver
的另一个框架。 实际上,Aurelia使用的polyfill是由Polymer团队开发的。 现在,可以在webcomponents.js项目中找到大多数Polymer的polyfills 。
Polymer中MutationObserver
的许多用法之一是监视容器的更改。 例如,如果应将样式作用域应用于容器及其所有后代,则变异观察者会监视更改并将相同的样式应用于可能的新元素。
scopeSubtree: function(container, shouldObserve) {
var scopify = function(node) {
// ...
};
scopify(container);
if (shouldObserve) {
var mo = new MutationObserver(function (mxns) {
mxns.forEach(function (m) {
if (m.addedNodes) {
for (var i = 0; i < m.addedNodes.length; i++)
scopify(m.addedNodes[i]);
}
});
});
mo.observe(container, { childList: true, subtree: true });
return mo;
}
}
上面的示例确保对元素的适当局部样式作用域,这些元素是在此局部范围内创建的,但不受容器的控制。 与第三方库一起使用时,这尤其有用。 当然,本机影子DOM实现将已经可以处理这些情况,而无需采取进一步的措施。
结论
变异观察者是跟踪DOM中变化的一种好方法。 它们提供了一种强大且快速的方式,以便在发生更改时得到通知。 即使对于浏览器扩展和MV *框架的作者来说,它们最有意义,但最好还是详细了解它们。 毕竟,这可能是我们最喜欢的库中的某些内容按其方式工作的原因。
在这里,我们总结了HTML5精通系列。 希望您在此过程中学到一些东西。 当然,并非所有项目都可以应用其中的某些技术,但在其中一个项目中可能会派上用场。 HTML5规范本身就很庞大。 各种扩展和补充材料甚至更大。 整个DOM规范是另一种野兽。
翻译自: https://code.tutsplus.com/tutorials/html5-mastery-dom-mutations--cms-24847