【如何监听容器高度变化(一)】 中介绍了几种使用模拟(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
2
3
4
5
6
7
8
9
10
<div id="main"></div>
<script>
var container = document.getElementById("main");
container.addEventListener("DOMSubtreeModified", function () {
console.log("DOMSubtreeModified"); // 控制台输出 DOMSubtreeModified
console.log(container.clientHeight); // 控制台输出 16
});
container.innerHTML =
'<img src="https://img30.360buyimg.com/babel/s590x470_jfs/t1/175659/2/14809/204433/60c88e44Eb9ffa27a/1cc8a1d4d8a5205b.jpg.webp" />';
</script>

上面的代码中,监听了 DOMSubtreeModified 事件,修改 dom 内容的时候被触发,但是也可以看到,使用这种方式也并没有获取到 dom 的正确高度。其实很好理解,因为图片是异步加载的,当 img 标签插入到 dom 中的时候,图片并没有加载完成,高度没有撑开。因此,这种方式的适用情况依然是有限度的,对于需要异步加载的资源不适用。

Mutation Events 存在很多问题,在 DOM4 中已经被废弃:

  1. 兼容性问题:
    Mutation Events兼容
  • 上图标注1的浏览器不支持 DOMAttrModified
  • 上图标注2的浏览器不支持 DOMNodeInsertedIntoDocument 和 DOMNodeRemovedFromDocument
  1. 性能问题
  • 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的兼容性如下:
MutationObserver兼容性

上图中,中间有-的黄色矩形表示需要加webkit前缀。

构造函数

MutationObserver()

使用时,首先使用 MutationObserver 构造函数,新建一个观察器实例,同时指定这个实例的回调函数。该回调函数接受两个参数,一个是变动的数组,另一个是观察器实例。

1
2
3
4
5
6
7
var observer = new MutationObserver(function(mutations, observer) {
console.log(observer)
for (var i = 0; i < mutations.length; i++) {
var mutation = mutations[i];
console.log(mutation);
}
});

方法

  1. observe()

observe 方法用来启动监听,接受两个参数,一个是要监听的 dom 节点,一个是配置项 options。

options 中可以配置要监听的变动类型(必须有其中的一种或几种,否则会报错),如下:

  • childList:布尔值,表示子节点的变动(指新增,删除或者更改)
  • attributes:布尔值,表示属性的变动
  • characterData:布尔值,表示节点内容或节点文本的变动(比如可以监听 input 值的变化)

此外,还可以设置以下属性:

  • subtree:布尔值,表示是否将该观察器应用于该节点的所有后代节点
  • attributeOldValue:布尔值,表示观察 attributes 变动时,是否需要记录变动前的属性值
  • characterDataOldValue:布尔值,表示观察 characterData 变动时,是否需要记录变动前的值
  • attributeFilter:数组,表示需要观察的特定属性(比如[‘class’,’src’])
1
2
3
4
5
6
7
8
9
var container = document.getElementById("main");
observer.observe(container, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true,
});

对一个节点添加观察器,就像使用 addEventListener 方法一样,多次添加同一个观察器是无效的,回调函数依然只会触发一次。但是,如果指定不同的 options 对象,就会被当作两个不同的观察器。

  1. disconnect()

disconnect 方法用来停止观察。调用该方法后,dom 再发生变动,也不会触发观察器。

1
observer.disconnect();
  1. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="main"></div>
<script>
var container = document.getElementById("main");
var currentHeight = container.clientHeight;
var observer = new MutationObserver(function (mutations, observer) {
var newHeight = container.clientHeight;
if (currentHeight !== newHeight) {
console.log("高度变化了:" + newHeight); // 控制台输出 高度变化了:16
currentHeight = newHeight;
}
});
observer.observe(container, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
});
container.innerHTML =
'<img src="https://img30.360buyimg.com/babel/s590x470_jfs/t1/175659/2/14809/204433/60c88e44Eb9ffa27a/1cc8a1d4d8a5205b.jpg.webp" />';
</script>

MutationObserver 和 Mutation Event 一样都是监听 dom 节点的变化,同样不能监听到异步资源加载后高度的变化。另外,因为 MutationObserver 监听的范围不包含样式属性的变化,因此如果通过 CSS 动画改变容器高度的话是检测不到的。我们需要在动画(transitionend、animationend)停止事件触发时监听高宽变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<style>
@keyframes changeHeight {
from,
to {
height: 100px;
}
50% {
height: 300px;
}
}
</style>
<div id="main" style="height: 100px; background: red; animation: changeHeight ease-in-out 5000ms"></div>
<script>
var container = document.getElementById("main");
container.addEventListener("animationend", function () {
console.log(getComputedStyle(container).height); // 控制台输出 100px
});
</script>

综上,MutationObserver 的事件处理是异步的,调用也比较简单,但仍有以下局限:

  1. IE10 及以下版本不兼容,需要和 Mutation Events 配合使用
  2. 不能监听异步资源加载完成后造成的高度变化,不能监听 flex 布局挤压造成的高度变化
  3. CSS动画造成的高度变化需要使用 transitionend或animationend 方法监听停止后的高度,动画进行中的高度变化监听不到

ResizeObserver

ResizeObserver 是一个实验中的功能,可以监听到 Element 的内容区域或 SVGElement的边界框改变。内容区域则需要减去内边距padding。

ResizeObserver 避免了在自身回调中调整大小,从而触发的无限回调和循环依赖。它仅通过在后续帧中处理 dom 中更深层次的元素来实现这一点。如果(浏览器)遵循规范,只会在绘制前或布局后触发调用。

ResizeObserver 的兼容性如下:
ResizeObserver兼容性

构造函数

ResizeObserver()

首先使用 ResizeObserver 构造函数,新建一个观察器实例,同时指定这个实例的回调函数。该回调函数接受一个参数,一个是变动的数组,另一个是观察器实例。

1
2
3
4
5
6
7
var observer = new ResizeObserver(function(entries, observer) {
console.log(observer)
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
console.log(entry);
}
});

构造函数回调中 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的大小,(同上)

方法

  1. observe()

observe 方法用来启动监听,接受一个参数,即要监听的 dom 节点。

1
2
var container = document.getElementById("main");
observer.observe(container);
  1. unobserve()

observe 方法用来停止监听,接收一个参数,即要停止监听的 dom 节点。

1
observer.observe(container);
  1. disconnect()

disconnect 方法用来停止 observer 下所有的监听。

1
observer.disconnect();

用 ResizeObserver 监听容器高度变化的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="main" style="border: 10px solid red; padding: 40px"></div>
<script>
var container = document.getElementById("main");
var currentHeight = container.clientHeight;
var observer = new ResizeObserver(function (entries, observer) {
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var newHeight = entry.borderBoxSize[0].blockSize;
if (newHeight !== currentHeight) {
console.log("高度变化了:" + newHeight); // 控制台输出 高度变化了:573
currentHeight = newHeight;
}
}
});
observer.observe(container);
container.innerHTML =
'<img src="https://img30.360buyimg.com/babel/s590x470_jfs/t1/175659/2/14809/204433/60c88e44Eb9ffa27a/1cc8a1d4d8a5205b.jpg.webp" />';
</script>

此外,对 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。)

总结

结合上一篇文章【如何监听容器高度变化(一)】 的内容,如果要监听容器高度的实时变化,推荐以下三种:

  1. 使用 object 模拟
  2. 监听 scroll 事件
  3. 使用 ResizeObserver + polyfill

当然,如果只需要知道窗口的实时高度,使用 resize 方法就行,不过要记得使用节流函数。