ResizeObserver:类似于元素的 document.onresize

ResizeObserver 可在元素的大小发生变化时通知您。

ResizeObserver 之前,您必须将监听器附加到文档的 resize 事件,才能在视口尺寸发生任何变化时收到通知。在事件处理程序中,您随后必须确定哪些元素受到了该更改的影响,并调用特定例程来做出适当的反应。如果您需要在调整大小后获取元素的新尺寸,则必须调用 getBoundingClientRect()getComputedStyle(),如果您不注意批量处理所有读取和所有写入,可能会导致布局抖动。

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

这就是 ResizeObserver 是一种实用基元的原因。它会对任何被观测元素的大小变化做出反应,而无论变化是由什么原因造成的。它还提供对观测到的元素的新尺寸的访问权限。

Browser Support

  • Chrome: 64.
  • Edge: 79.
  • Firefox: 69.
  • Safari: 13.1.

Source

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 框模型示意图。

请务必注意,虽然 ResizeObserver 会同时报告 contentRect 的尺寸和内边距,但它只会监听 contentRect请勿将 contentRect 与元素的边界框混淆。getBoundingClientRect() 报告的边界框是包含整个元素及其后代的框。SVG 是此规则的例外情况,其中 ResizeObserver 将报告边界框的尺寸。

自 Chrome 84 起,ResizeObserverEntry 新增了三个属性,可提供更详细的信息。这些属性中的每一个都会返回一个 ResizeObserverSize 对象,其中包含 blockSize 属性和 inlineSize 属性。此信息是关于调用回调时观察到的元素。

  • borderBoxSize
  • contentBoxSize
  • devicePixelContentBoxSize

所有这些项都会返回只读数组,因为我们希望将来它们能够支持具有多个 fragment 的元素,这种情况会出现在多列场景中。目前,这些数组只会包含一个元素。

这些属性的平台支持有限,但 Firefox 已经支持前两个属性。

何时报告?

规范规定,ResizeObserver 应在绘制之前和布局之后处理所有调整大小事件。因此,ResizeObserver 的回调是更改网页布局的理想位置。由于 ResizeObserver 处理发生在布局和绘制之间,因此这样做只会使布局失效,而不会使绘制失效。

Gotcha

您可能会问自己:如果我在回调中将受观测元素的大小更改为 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 过度延迟渲染。