在【如何监听容器高度变化(一)】 中介绍了几种使用模拟(iframe/object/scroll)的方式获取容器实时高度的方法,这篇文章主要介绍可以监听变化的API。
Mutation Events
容器高度是由 dom 节点的操作(如插入、渲染、移除等)引起的,如果我们可以监听到 dom 节点的子节点、属性、文本节点等的变化,就可以获取到此时容器的高度,从而实现对于容器高度的监听。
Mutation Events 是 DOM3 中定义的针对 dom 节点更改的事件,支持的事件如下:
- DOMAttrModified:dom 属性变更
- DOMAttributeNameChanged:dom 属性名修改
- DOMCharacterDataModified:dom 文本数据发生修改
- DOMElementNameChanged:dom 元素名发生变化
- DOMNodeInserted:dom 节点插入
- DOMNodeInsertedIntoDocument:dom 节点插入
- DOMNodeRemoved:dom 节点删除
- DOMNodeRemovedFromDocument:dom 节点删除
- DOMSubtreeModified:dom 子元素修改
备注:W3C文档上 DOMNodeInsertedIntoDocument 会先于 DOMNodeInserted 触发,但是测试过程中插入节点时 DOMNodeInsertedIntoDocument 没有被触发,DOMNodeInserted 有被触发。另外,和 DOMNodeInserted 相比,DOMNodeInsertedIntoDocument 兼容的浏览器更少。DOMNodeRemovedFromDocument 的情况也是一样的。
1 | <div id="main"></div> |
上面的代码中,监听了 DOMSubtreeModified 事件,修改 dom 内容的时候被触发,但是也可以看到,使用这种方式也并没有获取到 dom 的正确高度。其实很好理解,因为图片是异步加载的,当 img 标签插入到 dom 中的时候,图片并没有加载完成,高度没有撑开。因此,这种方式的适用情况依然是有限度的,对于需要异步加载的资源不适用。
Mutation Events 存在很多问题,在 DOM4 中已经被废弃:
- 兼容性问题:
- 上图标注1的浏览器不支持 DOMAttrModified
- 上图标注2的浏览器不支持 DOMNodeInsertedIntoDocument 和 DOMNodeRemovedFromDocument
- 性能问题
- Mutation Events 是同步执行的,每次调用都需要从事件队列中取出事件,执行,然后事件队列中移除。如果事件触发频繁,上述步骤会多次调用,会对浏览器性能造成影响。
- Mutation Events 本身是事件,所以捕获是采用的是事件冒泡的形式,如果冒泡捕获期间又触发了其他的 Mutation Events,很有可能就会导致阻塞 Javascript 线程,甚至导致浏览器崩溃。
MutationObserver
MutationObserver 接口是 Mutation Events 功能的替代品,同样用于监视 dom 节点的更改。概念上,MutationObserver 可以理解为 dom 发生变动就会触发 Mutation Observer 事件。但是,它与事件有一个本质不同:事件是同步触发,也就是说,dom 的变动立刻会触发相应的事件;Mutation Observer 则是异步触发,dom 的变动并不会马上触发,而是要等到当前所有 dom 操作都结束才触发。
MutationObserver 的特点如下:
- 等待所有脚本任务完成后,才会运行(即异步触发方式)
- 把 dom 变动记录封装成一个数组进行处理,而不是一条条个别处理 dom 变动
- 既可以观察 dom 的所有类型变动,也可以指定只观察某一类变动
MutationObserver的兼容性如下:
上图中,中间有-的黄色矩形表示需要加webkit前缀。
构造函数
MutationObserver()
使用时,首先使用 MutationObserver 构造函数,新建一个观察器实例,同时指定这个实例的回调函数。该回调函数接受两个参数,一个是变动的数组,另一个是观察器实例。
1 | var observer = new MutationObserver(function(mutations, observer) { |
方法
- observe()
observe 方法用来启动监听,接受两个参数,一个是要监听的 dom 节点,一个是配置项 options。
options 中可以配置要监听的变动类型(必须有其中的一种或几种,否则会报错),如下:
- childList:布尔值,表示子节点的变动(指新增,删除或者更改)
- attributes:布尔值,表示属性的变动
- characterData:布尔值,表示节点内容或节点文本的变动(比如可以监听 input 值的变化)
此外,还可以设置以下属性:
- subtree:布尔值,表示是否将该观察器应用于该节点的所有后代节点
- attributeOldValue:布尔值,表示观察 attributes 变动时,是否需要记录变动前的属性值
- characterDataOldValue:布尔值,表示观察 characterData 变动时,是否需要记录变动前的值
- attributeFilter:数组,表示需要观察的特定属性(比如[‘class’,’src’])
1 | var container = document.getElementById("main"); |
对一个节点添加观察器,就像使用 addEventListener 方法一样,多次添加同一个观察器是无效的,回调函数依然只会触发一次。但是,如果指定不同的 options 对象,就会被当作两个不同的观察器。
- disconnect()
disconnect 方法用来停止观察。调用该方法后,dom 再发生变动,也不会触发观察器。
1 | observer.disconnect(); |
- takeRecords()
takeRecords 方法用来清除变动记录,即不再处理未处理的变动。该方法返回变动记录的数组。
1 | var changes = observer.takeRecords(); // 保存没有被处理的变动 |
MutationRecord
MutationObserver() 构造函数回调中的 mutation 是 MutationRecord 的实例,包含的属性如下:
- type:观察的变动类型(attribute、characterData或者childList)
- target:发生变动的 dom 节点
- addedNodes:新增的 dom 节点
- removedNodes:删除的 dom 节点
- previousSibling:前一个同级节点,如果没有则返回 null
- nextSibling:下一个同级节点,如果没有则返回 null
- attributeName:发生变动的属性。如果设置了 attributeFilter,则只返回预先指定的属性
- oldValue:变动前的值。这个属性只对 attribute 和 characterData 变动有效,如果发生 childList 变动,则返回 null
对 MutationObserver 进行了简单的了解之后,我们回到监听容器高度的需求上来:
1 | <div id="main"></div> |
MutationObserver 和 Mutation Event 一样都是监听 dom 节点的变化,同样不能监听到异步资源加载后高度的变化。另外,因为 MutationObserver 监听的范围不包含样式属性的变化,因此如果通过 CSS 动画改变容器高度的话是检测不到的。我们需要在动画(transitionend、animationend)停止事件触发时监听高宽变化。
1 | <style> |
综上,MutationObserver 的事件处理是异步的,调用也比较简单,但仍有以下局限:
- IE10 及以下版本不兼容,需要和 Mutation Events 配合使用
- 不能监听异步资源加载完成后造成的高度变化,不能监听 flex 布局挤压造成的高度变化
- CSS动画造成的高度变化需要使用 transitionend或animationend 方法监听停止后的高度,动画进行中的高度变化监听不到
ResizeObserver
ResizeObserver 是一个实验中的功能,可以监听到 Element 的内容区域或 SVGElement的边界框改变。内容区域则需要减去内边距padding。
ResizeObserver 避免了在自身回调中调整大小,从而触发的无限回调和循环依赖。它仅通过在后续帧中处理 dom 中更深层次的元素来实现这一点。如果(浏览器)遵循规范,只会在绘制前或布局后触发调用。
ResizeObserver 的兼容性如下:
构造函数
ResizeObserver()
首先使用 ResizeObserver 构造函数,新建一个观察器实例,同时指定这个实例的回调函数。该回调函数接受一个参数,一个是变动的数组,另一个是观察器实例。
1 | var observer = new ResizeObserver(function(entries, observer) { |
构造函数回调中 entry 为 ResizeObserverEntry 对象的实例,包含属性如下:
- target:大小发生变化的 dom 节点
- contentRect:dom 节点的 contentRect,包含 width/height/top/left/right/bottom/x/y
- borderBoxSize:dom 含边框的尺寸大小,为数组,数组中对象包含 blockSize/inlineSize(为高度/宽度)
- contentBoxSize:dom 内容区域大小(不含边框和 padding),(同上)
- devicePixelContentBoxSize:contentBoxSize * window.devicePixelRatio的大小,(同上)
方法
- observe()
observe 方法用来启动监听,接受一个参数,即要监听的 dom 节点。
1 | var container = document.getElementById("main"); |
- unobserve()
observe 方法用来停止监听,接收一个参数,即要停止监听的 dom 节点。
1 | observer.observe(container); |
- disconnect()
disconnect 方法用来停止 observer 下所有的监听。
1 | observer.disconnect(); |
用 ResizeObserver 监听容器高度变化的代码如下:
1 | <div id="main" style="border: 10px solid red; padding: 40px"></div> |
此外,对 flex 挤压和 CSS 动画造成的高度变化进行了测试,ResizeObserver 都可以监听到。
注意:网上有人反馈 ResizeObserver 回调内如果涉及到会导致 reflow 的设置,在 chrome 中可能会报错: Error:ResizeObserver loop limit exceeded。解决办法是在回调中增加 requestAnimationFrame 来进行节流处理。【在这里的评论部分】
综上,ResizeObserver 可以实现动态监听元素高度的需求,美中不足的是不兼容 IE。这点可以使用 polyfill 来弥补:resize-observer-polyfill,可以兼容到 IE9。(测试中发现 IE 浏览器中回调中 entry 参数中只有 target 和 contentRect 两个属性,不包含 borderBoxSize/contentBoxSize/devicePixelContentBoxSize。)
总结
结合上一篇文章【如何监听容器高度变化(一)】 的内容,如果要监听容器高度的实时变化,推荐以下三种:
- 使用 object 模拟
- 监听 scroll 事件
- 使用 ResizeObserver + polyfill
当然,如果只需要知道窗口的实时高度,使用 resize 方法就行,不过要记得使用节流函数。