WebKit 中的 ResizeObserver

多年来,Web 开发者一直渴望能够设计出响应其容器而非视口的组件。开发者习惯于针对视口宽度使用媒体查询来实现响应式设计,但由于可能导致循环依赖,在 CSS 中基于元素大小使用媒体查询是不可能的。因此,需要一个 JavaScript 解决方案。

ResizeObserver 的引入解决了这个问题,它允许作者观察元素布局大小的变化。它于 2018 年 1 月首次在 Chrome 64 中可用,现在已在 Safari 技术预览版(和 Epiphany 技术预览版)中发布。从 Safari 技术预览版 97 开始,ResizeObserver 默认启用。

API 概述

脚本创建 ResizeObserver 实例时会带有一个回调函数,该函数将接收“观察结果”;它使用 .observe(element).unobserve(element) 来注册/注销回调。每次调用 observe(element) 都会将该元素添加到此 ResizeObserver 实例观察的元素集合中。

提供给构造函数的回调函数会接收一个观察条目集合,其中包含被观察的 CSS 盒子的状态数据(如果这些盒子实际改变了大小)。观察器本身还具有一个 .disconnect() 方法,用于停止向回调函数主动传递观察到的变化。下面是一个简单示例:

const callback = (entries) => {
  console.log(`${entries.length} resize observations happened`)
  Array.from(entries).forEach((entry) => {
    let rect = entry.contentRect;
    console.log(
      entry.target,
      `size is now ${rect.width}w x ${rect.height}h`
    )
  })
}

const myObserver = new ResizeObserver(callback)

myObserver.observe(targetElementA)
myObserver.observe(targetElementB)

我们用 ResizeObserver 观察的是*我们已观察到的 CSS 盒子大小的变化*。由于我们之前在观察这些盒子之前没有任何信息,而现在有了,这便产生了可观察的效果。假设 targetElementAtargetElementB 位于 DOM 中,我们将看到一条日志,显示发生了 2 次大小调整观察,并提供有关每个元素及其大小的一些信息。它看起来会像这样:

"2 resize observations happened"
"<div class='a'>a</div>" "size is now 1385w x 27h"
"<div class='b'>b</div>" "size is now 1385w x 27h"

同样,这意味着虽然观察一个不在 DOM 树中的元素不是*错误*,但在盒子实际布局(当它被插入并创建盒子时)之前不会发生任何观察。从 DOM 树中移除一个被观察的元素(未隐藏的)也会导致一次观察。

观察结果如何交付

ResizeObserver 严格规定了何时以及如何发生事情,并试图确保计算和观察总是在树中“向下”发生,以帮助作者避免循环。下面是其发生方式:

  1. 盒子被创建。
  2. 布局发生。
  3. 浏览器启动渲染更新,并运行直到包含 Intersection Observer 步骤的所有步骤。
  4. 系统收集并比较被观察元素的盒子大小与它们之前记录的大小。
  5. 调用 ResizeObserver 回调函数,并传递包含新大小信息的 ResizeObserverEntry 对象。
  6. 如果在回调期间发生任何更改,则会再次进行布局,但此时,系统会找到发生更改的最浅深度(从根节点计算的简单节点深度)。任何与树中更深层相关联的更改会立即交付,而任何不相关的更改则会被排队并在下一帧中交付,并且会将一条错误消息发送到 Web Inspector 控制台:(ResizeObserver loop completed with undelivered notifications)。
  7. 执行渲染更新中的后续步骤(即绘制发生)。

注意

在 Safari 技术预览版中,条目包含一个 .contentRect 属性,它反映了内容盒(Content Box)的大小。在早期反馈之后,规范正在以向后兼容的方式迭代,这也将提供一种获取边框盒(Border Box)度量值的方法。此 API 的未来版本还将允许 .observe 接受一个可选的第二个参数,允许您指定希望接收信息是关于内容盒还是边框盒。

实用示例

假设我们有一个包含作者个人资料的组件。它可能在具有多种尺寸屏幕的设备上以及多种布局上下文中使用。它甚至可能以某种方式作为自定义元素提供以供重用。此外,这些尺寸在运行时可能由于多种原因而发生变化:

  • 在桌面设备上,用户调整其窗口大小
  • 在移动设备上,用户改变屏幕方向
  • 一个新的元素出现,或从 DOM 树中移除,导致重新布局
  • DOM 中的其他元素由于任何原因改变大小(有些元素甚至用户可调整大小)

根据我们在任何给定时间可用的空间量,我们希望应用不同的 CSS——以不同的方式布局,改变一些字体大小,甚至可能使用不同的颜色。

为此,我们假设我们遵循“响应式优先”的理念,并为最小的屏幕尺寸进行初始设计。随着可用空间变大,我们有另一种设计应在有 768 像素可用时生效,还有一种在至少有 1024 像素可用时生效。我们将使用类名 “.container-medium” 和 “.container-large” 来实现这些设计。现在我们所要做的就是自动添加或移除这些类。

/* Tell the observer how to manage the attributes */
const callback = (entries) => {
  entries.forEach((entry) => {
    let w = entry.contentRect.width
    let container = entry.target

    // clear out any old ones
    container.classList.remove('container-medium', 'container-large')

    // add one if a 'breakpoint' is true
    if (w > 1024) {
      container.classList.add('container-large')
    } else if (w > 768) {
      container.classList.add('container-medium')
    }
  }) 
}

/* Create the instance **/
const myObserver = new ResizeObserver(callback)

/* Find the elements to observe */
const profileEls = [...document.querySelectorAll('.profile')]

/* .observe each **/
profileEls.forEach(el => myObserver.observe(el))

现在,如果每个 .profile 元素的可用大小符合我们指定的条件,它们将获得 .container-medium.container-large 类,并且我们的设计将始终根据其可用大小适当应用。当然,您可以将其与 MutationObserver 结合使用或作为一个自定义元素,以处理可能稍后出现的元素。

反馈

我们很高兴 ResizeObserver 在 Safari 技术预览版中可用!请尝试使用它,并报告您遇到的任何问题。