ResizeObserver:类似于元素的 document.onresize

当元素的大小发生变化时,ResizeObserver 会发出通知。

ResizeObserver 之前,您必须向文档的 resize 事件附加监听器,以便在视口尺寸有任何变化时收到通知。在事件处理脚本中,您必须找出哪些元素受到了该变化的影响,并调用特定例程来做出相应反应。如果您在调整大小后需要元素的新尺寸,则必须调用 getBoundingClientRect()getComputedStyle(),如果您不处理所有读取和所有写入操作,这可能会导致布局抖动。

这甚至不涵盖元素在未调整主窗口大小的情况下更改其大小的情况。例如,附加新的子项、将元素的 display 样式设置为 none 或类似操作,都可能会更改元素、其同级或祖先实体的大小。

正因如此,ResizeObserver 才是一个实用的基元。它会对任何观察到的元素的大小变化做出响应,与导致变化的原因无关。它还支持访问所观察元素的新大小。

浏览器支持

  • 64
  • 79
  • 69
  • 13.1

来源

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 对象。内容框是用于放置内容的框。它是边框减去内边距。

CSS box 模型的示意图。

请务必注意,虽然 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 之前,没有可靠的方法在其尺寸发生变化时收到通知,以便再次布局其子项。

对 Interaction to Next Paint 的影响 (INP)

Interaction to Next Paint (INP) 是一个指标,用于衡量网页对用户互动的总体响应情况。如果网页的 INP 处于“良好”阈值内(即 200 毫秒或更短),则可以说该网页能够可靠地响应用户与它之间的互动。

虽然为响应用户互动而运行事件回调所需的时间会显著影响互动的总延迟时间,但这不是 INP 需要考虑的唯一方面。INP 还会考虑交互的下一次绘制所需的时间。这是更新界面以响应互动完成所需的渲染工作所需的时间。

ResizeObserver 而言,这一点很重要,因为 ResizerObserver 实例运行的回调就在渲染工作之前发生。这是特意设计的,因为必须考虑回调中发生的工作,因为该工作很可能需要更改界面。

请注意尽可能少地执行 ResizeObserver 回调中所需的渲染工作,因为过多的渲染工作可能会导致浏览器延迟执行重要工作。例如,如果任意互动具有导致运行 ResizeObserver 回调的回调,请务必执行以下操作,以便提供尽可能流畅的体验:

  • 确保 CSS 选择器尽可能简单,以避免进行过多的样式重新计算工作。 样式重新计算发生在布局之前,并且复杂的 CSS 选择器可能会延迟布局操作。
  • 避免在 ResizeObserver 回调中执行任何可能会触发强制重排的工作。
  • 更新页面布局所需的时间通常会随着页面上的 DOM 元素数量而增加。无论页面是否使用 ResizeObserver,情况都是如此,但随着页面结构复杂性的增加,在 ResizeObserver 回调中完成的工作可能会变得很明显。

总结

ResizeObserver 适用于所有主流浏览器,它提供了一种高效监控元素级元素大小的方法。只是要小心,不要使用以下强大的 API 过多地延迟渲染。