ResizeObserver
可让您知道元素的大小发生变化时。
在 ResizeObserver
之前,您必须将监听器附加到文档的 resize
事件,才能在视口尺寸发生任何变化时收到通知。然后,您必须在事件处理脚本中找出受该更改影响的元素,并调用特定例程以做出适当响应。如果您需要在调整大小后获取元素的新尺寸,则必须调用 getBoundingClientRect()
或 getComputedStyle()
,如果您不注意批处理所有读取和所有写入,则可能会导致布局抖动。
这甚至不涵盖在未调整主窗口大小的情况下元素更改大小的情况。例如,附加新子项、将元素的 display
样式设置为 none
或执行类似操作,都可能会更改元素及其同级兄弟元素或祖先元素的大小。
这就是 ResizeObserver
非常有用的原因。它会对任何被观察元素的大小变化做出响应,而不会受到导致更改的原因的影响。它还提供对所观察元素的新大小的访问权限。
API
我们上面提到的所有带有 Observer
后缀的 API 都采用了简单的 API 设计。ResizeObserver
也不例外。您可以创建 ResizeObserver
对象,并将回调传递给构造函数。系统会向该回调函数传递 ResizeObserverEntry
对象数组(每个观察到的元素一个条目),其中包含该元素的新维度。
var ro = new ResizeObserver(entries => {
for (let entry of entries) {
const cr = entry.contentRect;
console.log('Element:', entry.target);
console.log(`Element size: ${cr.width}px x ${cr.height}px`);
console.log(`Element padding: ${cr.top}px ; ${cr.left}px`);
}
});
// Observe one or multiple elements
ro.observe(someElement);
一些详细信息
报告内容
通常,ResizeObserverEntry
会通过一个名为 contentRect
的属性报告元素的内容框,该属性会返回 DOMRectReadOnly
对象。内容框是用于放置内容的框。它是边框盒减去内边距。
请务必注意,虽然 ResizeObserver
会报告 contentRect
的尺寸和内边距,但它只会监控 contentRect
。请勿将 contentRect
与元素的边界框混淆。getBoundingClientRect()
报告的边界框是包含整个元素及其子元素的框。SVG 是此规则的例外情况,在这种情况下,ResizeObserver
会报告边界框的尺寸。
从 Chrome 84 开始,ResizeObserverEntry
新增了三个属性,可提供更详细的信息。这两个属性都会返回一个包含 blockSize
属性和 inlineSize
属性的 ResizeObserverSize
对象。此信息与调用回调函数时的被观察元素相关。
borderBoxSize
contentBoxSize
devicePixelContentBoxSize
所有这些项都会返回只读数组,因为我们希望将来它们能够支持具有多个 fragment 的元素(在多列场景中会出现)。目前,这些数组只会包含一个元素。
平台对这些属性的支持有限,但 Firefox 已支持前两个属性。
报告时间是什么时候?
规范规定,ResizeObserver
应在绘制前和布局后处理所有大小调整事件。这使得 ResizeObserver
的回调成为更改页面布局的理想位置。由于 ResizeObserver
处理发生在布局和绘制之间,因此这样做只会使布局失效,而不会使绘制失效。
好问题
您可能会问自己:如果我将回调内被观察元素的大小更改为 ResizeObserver
,会发生什么情况?答案是:您将立即触发对回调函数的另一个调用。幸运的是,ResizeObserver
具有避免无限回调循环和循环依赖项的机制。只有当调整大小的元素在 DOM 树中的深度大于上一个回调中处理的最浅元素时,系统才会在同一帧中处理更改。否则,它们会被推迟到下一帧。
应用
ResizeObserver
的一个用途是实现按元素的媒体查询。通过观察元素,您可以命令方式定义设计断点并更改元素的样式。在以下示例中,第二个框将根据其宽度更改边框半径。
const ro = new ResizeObserver(entries => {
for (let entry of entries) {
entry.target.style.borderRadius =
Math.max(0, 250 - entry.contentRect.width) + 'px';
}
});
// Only observe the second box
ro.observe(document.querySelector('.box:nth-child(2)'));
另一个有趣的示例是聊天窗口。典型的自上而下对话布局中出现的问题是滚动定位。为避免让用户感到困惑,最好让该窗口固定在对话底部(最新消息显示的位置)。此外,任何类型的布局更改(例如手机从横屏模式切换到竖屏模式或反之)都应实现相同的效果。
借助 ResizeObserver
,您可以编写一段代码来处理这两种情况。按定义,窗口大小调整是 ResizeObserver
可以捕获的事件,但调用 appendChild()
也会调整该元素的大小(除非设置了 overflow: hidden
),因为它需要为新元素腾出空间。因此,只需几行代码即可实现所需效果:
const ro = new ResizeObserver(entries => {
document.scrollingElement.scrollTop =
document.scrollingElement.scrollHeight;
});
// Observe the scrollingElement for when the window gets resized
ro.observe(document.scrollingElement);
// Observe the timeline to process new messages
ro.observe(timeline);
挺酷的,对吧?
接下来,我可以添加更多代码来处理以下情况:用户手动滚动到顶部,并希望在有新消息时滚动到该消息。
另一种用例是,用于执行自己的布局的任何类型的自定义元素。在 ResizeObserver
之前,没有可靠的方法可以在其尺寸发生变化时收到通知,以便其子项重新布局。
对下一次绘制的交互的影响 (INP)
Interaction to Next Paint (INP) 是一项指标,用于衡量网页对用户互动的总体响应情况。如果网页的 INP 在“良好”阈值(即 200 毫秒或更短)内,则可以说该网页对用户的互动有可靠的响应。
虽然事件回调为响应用户互动而运行所需的时间会显著增加互动的总延迟时间,但这并不是 INP 需要考虑的唯一方面。INP 还会考虑发生互动的下一次绘制所需的时间。这是响应互动更新界面所需的渲染工作完成所需的时间。
对于 ResizeObserver
,这一点很重要,因为 ResizerObserver
实例运行的回调会在渲染工作之前发生。这是有意为之,因为必须考虑回调中发生的工作,因为该工作很可能会导致界面发生更改。
请务必在 ResizeObserver
回调中尽可能少执行渲染工作,因为过多的渲染工作可能会导致浏览器延迟执行重要工作。例如,如果任何互动都有导致 ResizeObserver
回调运行的回调,请务必执行以下操作,以便尽可能提供流畅的体验:
- 确保 CSS 选择器尽可能简单,以避免过度重复计算样式。样式重新计算会在布局之前进行,复杂的 CSS 选择器可能会延迟布局操作。
- 避免在
ResizeObserver
回调中执行任何可能会触发强制重排的工作。 - 更新页面布局所需的时间通常会随着页面上 DOM 元素的数量而增加。无论网页是否使用
ResizeObserver
,这一点都是正确的,但随着网页结构复杂性的增加,在ResizeObserver
回调中执行的工作量可能会变得非常大。
总结
所有主要浏览器中均提供 ResizeObserver
,并提供了一种在元素级别监控元素大小调整的有效方式。但请注意,不要让这个强大的 API 呈现太多延迟。