谁动了我的 DOM??!

2,056 阅读7分钟

元素的样式为何频频被改?

DOM 的属性为何离奇失踪??

消失的 DOM 究竟是何人所为???

出现的陌生 DOM 究竟是人是鬼????

这一切的背后是人性的扭曲还是道德的沦丧?????

是无意写的 Bug 还是有人故意而为之??????

让我们跟随镜头,探寻千变万化的 DOM。

起源

最近在做项目的时候,遇到一个问题:

就是一个很久以前写的页面,里面的代码很乱。而我的任务是将页面的高度与屏幕适配。在部分页面存在着几个 <iframe>,我需要调整样式使其高度与屏幕适配,但是无论我怎么调整,总会有一个 JavaScript 在不停地修改 <iframe> 的高度,使得它的高度超出屏幕而出现两个滚动条。

由于代码很乱,所以很难找到究竟是哪个代码在捣乱。

Chrome 中的解决方法

在 Chrome 中开发前端是非常开心的一件事情,因为浏览器提供了非常多的调试工具。而我经常用的一个就是:断点调试。

Chrome 提供了一个 DOM 监视的功能,当 DOM 发生变化的时候,自动暂停,这样就能很快定位是谁修改了 <iframe> 的高度了!

在 Elements 页面,选中指定的 DOM 节点,点击最前面的“...”符号(或者使用右键点击),在弹出的菜单中选择“Break On”即可,可以选择多个。

  • Subtree Modifications: 当节点树发生变化时
  • Attribute Modifications: 当节点属性发生变化时
  • Node Removal: 当节点被删除时(包括下级节点)

只要启用其中一个,在具体事件发生的时候,Chrome 就会自动中断到当前执行的脚本代码处。

由于 JavaScript 修改样式无非就是增加/删除 class,或是修改 style 属性,所以,使用 Attribute Modifications 即可找到具体是哪个脚本在修改 DOM 了。

新的问题

在做前端开发的时候,经常会想要监听 DOM 节点的变化。当 DOM 变化的时候,触发一系列事件。

一般来说,使用轮询的方式可以非常简单地解决这个问题,就是使用 setInterval 来不断地检查 DOM 是否发生了变化。这种方式简单粗暴,但是会遇到两个问题:时间间隔设置过长,DOM 变化响应不够及时;时间间隔设置过短,不仅浪费 CPU,而且可能出现卡顿。

当然,也可以使用 requestAnimationFrame 来做,原理其实和 setInterval 一样,只不过在一定程度上可以得到 DOM 变化实时响应,但是依旧是导致 CPU 运行时间片的浪费。

有没有更好的办法呢?

在旧版 DOM Events 标准中,有一个 Mutation events,可以用来监听 DOM 的变化,在 DOM 变化的时候触发事件。

在 DOM3 中定义了 9 种 Mutation 事件:DOMAttrModifiedDOMAttributeNameChangedDOMCharacterDataModifiedDOMElementNameChangedDOMNodeInsertedDOMNodeInsertedIntoDocumentDOMNodeRemovedDOMNodeRemovedFromDocumentDOMSubtreeModified

这 9 种事件可以直接通过 element.addEventListener 添加到 DOM 元素上。

但是,Mutation 事件已经被反对使用!并且从 Web 标准事件中删除了!

由于性能问题,Mutation 事件会导致 DOM 修改的性能降低 1.5~7 倍,并且不能通过移除事件来恢复性能。

并且这个事件在各个浏览器上的实现也存在差异。

所以,DOM4 开始,推荐使用 Mutation Observers 来代替 Mutation events

Mutation Observers

Mutation Observer API 可以用来监视 DOM 的变化,包括属性的变化、节点的增减、内容的变化等。

比 Mutation Events 高在哪里?

为什么 Mutation Observers 要比 Mutation Events 好?

由于 Mutation Events 是监视到 DOM 发生变化时产生的事件,它会在任何一个 DOM 发生变化的时候立刻被触发。并且,由于事件是同步进行的,所以如果 DOM 的变化较多,就会产生大量的事件回调,导致严重的性能问题。

Mutation Observers 虽然和 Mutation Events 很像,但是 Mutation Observers 不是事件,它是异步触发的,并且不是每次 DOM 变动都会触发,而是会等待多次 DOM 变动完成后一次性触发,使用一个数组来记录 DOM 变动的步骤。这样一来,即使是频繁的 DOM 操作,对性能的影响也不会有多明显。

举个例子,我现在需要将一篇包含 1000 个段落的文章显示到页面上,也就是要往页面中插入 1000 个 <p></p>

如果使用 Mutation Events 的话,这时就会产生 1000 个 DOMNodeInserted 事件;而如果使用 Mutation Observers 就不一样了,它只会触发一次,得到一个数组,包含了 1000 个插入节点的信息。

怎么用呢?

MutationObserver 是一个构造函数,可以使用 new 来创建一个 MutationObserver 的实例。这个构造函数接受一个回调函数作为参数,也就是每次 Mutation Observers 触发时调用的函数,函数接受两个参数,第一个参数是 MutationRecord 数组,用于存储 DOM 的变化记录,第二个参数是 MutationObserver 实例本身。

MutationObserver 的实例有 3 个成员方法:observedisconnecttakeRecords

observe 用于注册监听器,接受两个参数,第一个参数是要监听的节点,第二个参数是监听的配置。

监听配置是一个对象,可以有 childListattributescharacterDatasubtreeattributeOldValuecharacterDataOldValueattributeFilter,要监听哪种变化,只需要将对应的属性设置为 true 即可,其中 childListattributescharacterData 三者必须至少出现一个。

属性 数据类型 描述
childList boolean 观察目标增加或移除了子节点
attributes boolean 观察目标增加、删除或修改了某个属性
characterData boolean (目标为 characterData 节点时有效,包括文本节点、注释节点、处理指令节点等)文本内容发生了变化
subtree boolean 不仅监视 ovserve 第一个参数指定的观察目标,同时监视所有的下级节点
attributeOldValue boolean 在监视 attributes 的时候,属性发生变化后是否要记录变化前的内容
characterDataOldValue boolean 在监视 characterData 的时候,文本内容发生变化后是否要记录变化前的内容
attributeFilter Array<string> 一个属性名数组,可以用于过滤 attributes 的变化

注册成功后,构造函数里提供的回调函数将会被调用,第一个参数就得到了变化数组。变化对象的结构包含以下属性:

属性 数据类型 描述
type String 变化类型,对应监听配置对象中的 childListattributescharacterData
target Node 变化的目标节点,如果 typeattributescharacterData,则 target 为变化节点,否则为变化节点的父节点
addedNodes NodeList 被添加的节点列表(可能为 null
removedNodes NodeList 被删除的节点列表(可能为 null
previousSibling Node 被添加或被删除的节点的前一个兄弟节点(可能为 null
nextSibling Node 被添加或被删除的节点的后一个兄弟节点(可能为 null
attributeName String 变化的属性名称(可能为 null
attributeNamespace String 变化的属性所在的 XML 命名空间(可能为 null
oldValue String 如果 typeattributescharacterData,则 oldValue 为变化前的值,否则为 null

disconnect 用于停止监听。

takeRecords 用于清空并返回当前 MutationObserver 记录的 DOM 变化步骤。

浏览器兼容性

可以看到,兼容性还是非常好的,可以放心使用。

总结

MutationObserver 提供了比 Mutation Events 更高效、更灵活的 DOM 监视方案,可以根据自己的需要自定义监视对象,在组件化项目中可以发挥更大的价值——不需要组件内部提供接口,就可以收到组件内容变化的通知。

但是,MutationObserver 虽好,可不要滥用哦!

Chrome 提供的 Break On 功能看起来就像是 MutationObserver 的精简版,非常实用。


关注微信公众号:创宇前端(KnownsecFED),码上获取更多优质干货!