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 盒子大小的变化*。由于我们之前在观察这些盒子之前没有任何信息,而现在有了,这便产生了可观察的效果。假设 targetElementA
和 targetElementB
位于 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
严格规定了何时以及如何发生事情,并试图确保计算和观察总是在树中“向下”发生,以帮助作者避免循环。下面是其发生方式:
- 盒子被创建。
- 布局发生。
- 浏览器启动渲染更新,并运行直到包含 Intersection Observer 步骤的所有步骤。
- 系统收集并比较被观察元素的盒子大小与它们之前记录的大小。
- 调用
ResizeObserver
回调函数,并传递包含新大小信息的ResizeObserverEntry
对象。 - 如果在回调期间发生任何更改,则会再次进行布局,但此时,系统会找到发生更改的最浅深度(从根节点计算的简单节点深度)。任何与树中更深层相关联的更改会立即交付,而任何不相关的更改则会被排队并在下一帧中交付,并且会将一条错误消息发送到 Web Inspector 控制台:(
ResizeObserver loop completed with undelivered notifications
)。 - 执行渲染更新中的后续步骤(即绘制发生)。
注意
在 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 技术预览版中可用!请尝试使用它,并报告您遇到的任何问题。