探索 ResizeObserver 的神奇力量
观察者 API
家族,全部文章列表,欢迎点赞收藏
引言
-
是什么:介绍
ResizeObserver
技术基础知识、兼容性以及原理 -
做什么:介绍
ResizeObserver
在多场景的应用和实战代码,同时会介绍笔者总结的一些进阶技巧和最佳实践,其中应用场景多达 10 个:- 响应式组件
- 文本内容折叠并显示"...查看全部"
- 虚拟列表支持动态高度
- 拖拽和缩放
- 在容器尺寸变化时做出反应
- 感知交互行为的发生
- 感知元素是否显示或隐藏
- 响应式广告投放
- 聊天窗口最新消息置底
- 埋点真实性确认
-
哪些坑:介绍
ResizeObserver
怎么测试和调试以及一些限制
文章约 10000 字,阅读时长 30 分钟
1. 介绍
从一道面试题开始
题目:如何让一个组件或者一个元素做到响应式?
每个人都有自己的答案,这里分析仅供参考
答: 思考这个问题,最先想到的可能是 CSS
媒体查询,根据不同设备的屏幕大小、方向和其他特征来创建响应式布局和样式,可以用于整体页面也可以用于某个组件。
JS
方法可以选用 window.matchMedia
,效果类似 CSS
媒体查询。
更精细的控制,可以通过 window.resize
监听视口(viewport
)变化,然后通过getBoundingClientRect
或者 getComputedStyle
来获取此时我们关心的元素大小,以此判断元素是否发生了变化,但这些 API
会导致 reflow
,同时 resize
事件触发也有些频繁,有一定的性能问题。
ResizeObserver 出现的背景
仔细想一想上面的答案,相信很多小伙伴都会觉得有道理但没有抓住关键
性能问题先不考虑的情况下,上述三种方案依然存在一个致命问题:只能基于监测 viewport
的变化做出反应
试想下:有时候窗体的尺寸没有变化,DOM
元素的尺寸有没有可能变化?(此时组件同样需要做响应式调整)
场景其实很多:
- 最简单的,当动态添加或从
DOM
中删除元素时,会影响父元素的大小 - 一个可拖拽调整大小的容器被改变尺寸时
- 元素仅仅是显示隐藏切换时
反之 有的时候窗体的尺寸变化了,但是 DOM
元素的尺寸并没有变化,此时组件不需要做响应式调整
这些场景在现代 web
应用程序中非常频繁,通过以上分析,再加上性能考量,我们可以把问题精简为:
题目:如何高效的观察一个元素的尺寸变化?
为解决这个问题,一个全新的 API
就出来了,就是 ResizeObserver API
,专门用来观察 DOM
元素的尺寸是否发生了变化,同时无需再手动调用 getBoundingClientRect
来获取元素的尺寸大小,它对任何元素大小变化做出反应,与引起变化的原因无关。
ResizeObserver 基础使用
-
创建
ResizeObserver
实例:使用new ResizeObserver()
构造函数创建一个ResizeObserver
实例。可以将一个回调函数作为参数传递给构造函数,该回调函数将在尺寸变化时被调用。const observer = new ResizeObserver(callback);
-
观察目标元素:使用
ResizeObserver
实例的observe()
方法来观察需要监听尺寸变化的目标元素。可以传入要观察的元素作为参数。const targetElement = document.getElementById('target'); observer.observe(targetElement);
-
编写回调函数:定义一个回调函数,当目标元素的尺寸发生变化时将被触发。回调函数将接收一个参数,其中包含了尺寸变化的相关信息。
function callback(entries, observer) { entries.forEach(entry => { console.log('目标元素的尺寸发生变化'); console.log('新的宽度:', entry.contentRect.width); console.log('新的高度:', entry.contentRect.height); }); }
-
停止观察尺寸变化(可选):如果不再需要监听目标元素的尺寸变化,可以使用
ResizeObserver
实例的unobserve()
方法停止观察。observer.unobserve(targetElement);
完整示例代码如下:
const observer = new ResizeObserver(callback);
const targetElement = document.getElementById('target');
observer.observe(targetElement);
function callback(entries, observer) {
entries.forEach(entry => {
console.log('目标元素的尺寸发生变化');
console.log('新的宽度:', entry.contentRect.width);
console.log('新的高度:', entry.contentRect.height);
});
}
MDN
官方介绍和示例请移步
2 兼容性
2.1 原生支持度
浏览器支持程度超过 95%,IE11+ 以及其他现代浏览器直接引入项目 JS
就可以了。
2.2 polyfill
如果需要兼容更低版本的浏览器环境,你可能需要安装一个 polyfill
,以下是较为常用的ResizeObserver polyfill
浏览器版本支持情况如下:
另一个可选项是:resize-observer-polyfill
3 实战应用
3.1 响应式组件
第一个场景,是开篇面试题的解决方案 这里以 Chrome web.dev 的一个方案作为示例:
const ro = new ResizeObserver(entries => {
for (let entry of entries) {
entry.target.style.borderRadius = Math.max(0, 250 - entry.contentRect.width) + 'px';
}
});
ro.observe(document.querySelector('.box'));
这里展示了一个 box
元素根据其宽度更改边框半径,实际项目中有更复杂的逻辑和实现,但原理一致。
vue-responsive-components 库有具体实现代码,还有组件形式的实现,感兴趣的可以去看看
3.2 文本内容折叠并显示"...查看全部"
文字超出部分显示"...",这个效果大部分人可能会想到 css
实现,但加上折叠效果和点击"查看全部"展开效果,纯 css
很难完美实现这个功能,往往需要配合 JS
实现,这其中最重要的一个环节,就是随着传入的文本变动或者容器尺寸的变动要进行重新计算,这也正是 ResizeObserver
可以发挥的地方:
const observer = new ResizeObserver(() => {
refresh(); // 重新计算
});
observer.observe(document.getElementById('target'));
这个功能有一个实现的 vue 库,vue-overflow-ellipsis,全部实现代码可以参考
3.3 虚拟列表支持动态高度
虚拟列表的实现过程中,列表项的高度 是一个必备的数据,依赖于这些数据虚拟列表才可能正常的工作,通过计算机和我们算法的的算力以及即时渲染能力换取 DOM
数量的减少。
- 列表项定高的虚拟列表,往往在初始化的时候就需要把高度值传递给组件。但有时候我们需要的组件是一个自适应的组件,需要在不同设备用不同的高度展示,尤其是在
px-to-viewport
方案时,这时候预先传入的列表项高度就不准,需要根据屏幕比例计算后传给组件或者初始化完成之后再计算一次 - 列表项动态高度的虚拟列表
- 比如列表项是图片等资源需要一定加载时间之后才能确定高度
- 比如列表项是动态内容每一项高度不一致
- 比如列表项是标题+内容,内容是可以展开收起的
- 比如虚拟列表的变种,虚拟树形组件
如果考虑动态的高度需求,这时候对列表项高度的监控就是一项必须做的工作,这时候就是 ResizeObserver
的一个使用场景了,而且使用 ResizeObserver
以上两种场景可以一种方案同时解决。
以下是 vue
中列表项节点的组件实现示例:
<template>
<div ref="node">
<slot></slot>
</div>
</template>
<script>
// 向上找组件
import { findComponentUpward } from '@/utils/findComponents';
// 基于 requestAnimationFrame 的截流函数
import { rafThrottle } from '@/utils';
export default {
name: 'virtual-node',
props: {
index: {
type: Number,
},
},
mounted() {
const dom = this.$refs.node;
this.observer = new ResizeObserver(rafThrottle(this.insideViewportCb));
this.observer.observe(dom);
this.virtualList = findComponentUpward(this, 'virtual-list');
},
methods: {
insideViewportCb([entry]) {
const height = entry.contentRect.height;
if (height === 0) return;
const cacheHeight = this.virtualList.positions[this.index].height;
if (cacheHeight === height) return;
this.$emit('size-change', {
index: this.index,
height,
});
return;
},
},
};
</script>
3.4 拖拽和缩放
- 拖拽和调整元素大小
- 拖拽时多元素联动
- 拖放和布局编辑器
- 多栏现实时控制最小尺寸
- 图片裁剪和缩放 以上场景大同小异,以图片裁剪和缩放为例,展示代码:
// 获取图片容器元素和图片元素
const imageContainer = document.querySelector('.image-container');
const image = imageContainer.querySelector('img');
// 使用 ResizeObserver 监听容器尺寸变化
const resizeObserver = new ResizeObserver(function(entries) {
for (let entry of entries) {
// 获取容器的宽度和高度
const containerWidth = entry.contentRect.width;
const containerHeight = entry.contentRect.height;
// 计算图片的缩放比例
const imageWidth = image.naturalWidth;
const imageHeight = image.naturalHeight;
const scaleX = containerWidth / imageWidth;
const scaleY = containerHeight / imageHeight;
const scale = Math.max(scaleX, scaleY);
// 根据缩放比例设置图片的样式
const newWidth = Math.ceil(imageWidth * scale);
const newHeight = Math.ceil(imageHeight * scale);
const newLeft = Math.ceil((containerWidth - newWidth) / 2);
const newTop = Math.ceil((containerHeight - newHeight) / 2);
image.style.width = newWidth + 'px';
image.style.height = newHeight + 'px';
image.style.left = newLeft + 'px';
image.style.top = newTop + 'px';
}
});
resizeObserver.observe(imageContainer); // 监听图片容器的尺寸变化
以上代码会根据图片容器的尺寸变化,计算图片的缩放比例,并根据缩放比例设置图片的样式,以实现图片的裁剪和缩放效果。ResizeObserver API
会监听容器尺寸的变化,以实时更新图片的样式,确保图片始终填充并居中于容器中。
注意,这只是一个基本的示例,你可以根据自己的需求进行进一步的样式和交互优化,例如添加动画效果或调整图片的对齐方式。
3.5 在容器尺寸变化时做出反应
- 响应式地图和地图标注
- 自由绘制的画板
- 数据可视化随大小调整展示内容或间隔
- 响应式导航菜单
- 结合
CSS Animation API
实现动态布局调整 - 自适应弹窗和对话框
以上场景都是需要在父容器尺寸改变时做响应动作的场景,和3.1
、3.2
有些类似,但又有所不同,并不需要特别高的灵敏度但触发时机又需要精确控制,因此往往需要配合截流防抖或者其他控制逻辑一起使用,代码类似
3.6 感知交互行为的发生
这是 张鑫旭 大神的一个示例:
当元素里面的内容变多或变少的时候,如果没有把高度和宽度定死,则我们可以通过观察元素的尺寸是否有变化,而知道是否有交互行为发生。 例如,我们在使用
Ajax
发表评论的时候,需要把评论写入到现有的评论列表,通过观察评论容器的尺寸变化,我们就可以认为这个评论交互已经完成,然后脱离具体的业务逻辑完成其他一些需求,例如数据埋点,我们只需要观察特定容器尺寸是否变化,然后发送埋点数据即可。优点是数据埋点无侵入,无需写入到业务逻辑中,非常灵活,也利于日后维护。
原文链接在此
3.7 感知元素是否显示或隐藏
另一个 张鑫旭 大神的一个示例:
当一个元素使用
display:none
进行隐藏的时候,也是会触发尺寸变化的,于是也能够被观测到。 基于尺寸的观测要比基于属性的观测要更精准。因为一个元素的隐藏可能是通过其他元素的属性变化触发的,例如其他元素添加了一个类名,这个类名正好可以影响当前元素的隐藏。
原文链接在此
const objResizeObserver = new ResizeObserver(function () {
if (getComputedStyle(img).display == 'none') {
console.log('图片隐藏了');
} else {
console.log('图片显示了');
}
});
// 观察图片元素
objResizeObserver.observe(img);
3.8 响应式广告投放
响应式广告投放是一种广告投放策略,旨在根据用户的设备、屏幕尺寸和浏览器等特征,提供适应性更强的广告体验。它的目标是确保广告在各种设备上都能正确呈现,并在不同屏幕尺寸上保持良好的可视性和用户体验。 响应式广告投放使广告主可以更好地达到跨设备的广告覆盖,提高广告的可见性和点击率,并为用户提供一致且良好的广告体验。同时,响应式广告投放还简化了广告创作和管理的流程,减少了针对不同设备和屏幕尺寸进行独立投放的工作量和复杂性。
// 获取广告容器元素和广告占位元素
const adContainer = document.querySelector('.ad-container');
const adPlaceholder = adContainer.querySelector('.ad-placeholder');
// 创建 ResizeObserver 实例
const resizeObserver = new ResizeObserver(function(entries) {
for (let entry of entries) {
// 获取容器的宽度和高度
const containerWidth = entry.contentRect.width;
const containerHeight = entry.contentRect.height;
// 根据容器的尺寸调整广告的大小和展示方式
if (containerWidth >= 768) {
// 大屏幕尺寸,显示大尺寸广告
adPlaceholder.style.backgroundImage = 'url(big-ad.jpg)';
adPlaceholder.style.backgroundSize = 'cover';
} else {
// 小屏幕尺寸,显示小尺寸广告
adPlaceholder.style.backgroundImage = 'url(small-ad.jpg)';
adPlaceholder.style.backgroundSize = 'contain';
}
}
});
resizeObserver.observe(adContainer); // 监听广告容器的尺寸变化
注意:此示例仅展示了根据尺寸展示不同广告的场景,实际场景要复杂的多,往往设计数据、文案、样式以及接口等等复杂逻辑
3.9 聊天窗口最新消息置底
这是来自 Chrome web.dev
的一个示例:
一个有趣的例子是聊天窗口。在典型的从上到下的对话布局中出现的问题是滚动定位。为避免让用户感到困惑,最好将窗口贴在对话的底部,即最新消息出现的位置。此外,任何类型的布局更改(设想手机从横向变为纵向,反之亦然)都应该实现相同的效果。 利用
ResizeObserver
只需写一段代码,即可满足两种情况的需要。调整窗口大小是一个ResizeObserver
可以根据定义捕获的事件,但调用appendChild()
也会调整该元素的大小(除非设置了overflow: hidden
),因为它需要为新元素腾出空间。考虑到这一点,只需很少的行就可以达到预期的效果:
const ro = new ResizeObserver(entries => {
document.scrollingElement.scrollTop =
document.scrollingElement.scrollHeight;
});
// 监听 scrollingElement 了解窗口大小何时改变
ro.observe(document.scrollingElement);
// 监听 timeline 以处理新消息
ro.observe(timeline);
3.10 埋点真实性确认
有些时候,我们对一些埋点进行真实性确认,比如有一个 A B 实验埋点,用户命中 A 需要展示 boxA 元素,命中 B 需要展示 boxB 元素,但因为网络、缓存等原因,上报的命中结果和真实命中结果可能会有差别。其他场景类似,比如在 300px 展示 box300,500px 展示 box500 等场景。
这时候 ResizeObserver
可以提供这种观察,并且提供和业务解耦的确认(真实上报)逻辑。
// 假设当前 AB 结果为 A
const objResizeObserver = new ResizeObserver(function () {
if (getComputedStyle(img).display == 'none') {
trackExposure('ABTest', false); // 记录埋点无效
} else {
trackExposure('ABTest', true); // 记录埋点有效
}
});
// 观察元素
objResizeObserver.observe(document.getElementById('boxA'));
4 进阶技巧和最佳实战
ResizeObserver
是一个强大的 API
,除了基本的用法,还有一些进阶技巧和最佳实战可以帮助你更好地应用 ResizeObserver
。下面是一些值得注意的技巧和实践:
-
避免频繁触发回调 当元素的尺寸发生变化时,
ResizeObserver
会立即触发回调函数。为了避免频繁触发回调,可以使用防抖或节流技术来控制回调的触发频率,以提高性能和优化用户体验。 -
监听根元素尺寸变化 除了监听单个元素的尺寸变化,
ResizeObserver
还可以监听根元素(如<body>
或<html>
)的尺寸变化。这对于需要响应整个页面尺寸变化的场景非常有用,例如实现响应式布局或全局样式的调整。 -
处理动态添加的元素 如果页面中存在动态添加的元素,需要在元素被添加到
DOM
后手动调用observe
方法来开始监听其尺寸变化。同样,在元素被移除时,记得调用unobserve
方法停止监听。 -
结合其他 API 使用
ResizeObserver
可以与其他Web API
结合使用,增强其功能。例如,结合Intersection Observer API
可以实现元素的可见性检测,结合Mutation Observer API
可以监测DOM
结构的变化。 -
性能优化 如果页面中存在大量需要监听尺寸变化的元素,为了避免性能问题,可以限制
ResizeObserver
的实例数量,或者只在需要的时候才创建实例。同时,可以选择性地监听某些关键元素的尺寸变化,而忽略其他不重要的元素。 -
兼容性处理 尽管
ResizeObserver API
是现代浏览器的标准API
,但为了确保在旧版本的浏览器中的兼容性,可以使用polyfill
或库来提供类似ResizeObserver
的功能。 -
销毁 ResizeObserver 实例 当不再需要监听尺寸变化时,记得手动调用
disconnect
方法来停止ResizeObserver
的监听,以释放资源和避免潜在的内存泄漏。 -
循环调用 在回调函数中对元素的尺寸进行修改可能会导致循环调用的问题。因为每次尺寸变化都会触发
ResizeObserver
的回调函数,如果在回调函数中又修改了元素的尺寸,会再次触发回调,造成无限循环。为了避免这个问题,需要谨慎处理回调函数中对元素尺寸的修改操作。
5 总结
ResizeObserver API
的目标是提供一种高效且准确地监听 DOM
元素尺寸变化的方法,以满足开发者在动态布局和自适应界面方面的需求。
ResizeObserver
具有以下几个特性:
- 监听元素尺寸变化
ResizeObserver
主要用于监听指定元素的尺寸变化。它可以观察目标元素的宽度、高度、边界框等尺寸属性的变化,并在发生变化时触发回调函数。 - 高性能和效率
ResizeObserver
设计用于高性能和效率的尺寸变化检测。相对于传统的resize
事件或MutationObserver
,ResizeObserver
只关注被观察元素的尺寸变化,避免了不必要的计算和触发,提供了更优化的性能。 - 同时观察多个元素
ResizeObserver
允许一次性观察多个元素的尺寸变化。通过将多个目标元素传递给observe
方法,可以同时监听它们的尺寸变化,而不需要为每个元素单独创建监听器。 - 提供准确的尺寸信息
在回调函数中,
ResizeObserver
提供了准确的尺寸信息。这包括目标元素的宽度、高度、上下左右边界的偏移量等,使开发者可以根据需要进行动态布局或样式调整。 - 触发方式灵活可控
ResizeObserver
的触发方式灵活可控。它可以通过设置不同的触发选项来决定在什么情况下触发回调函数,例如只在尺寸变化结束后触发、只在尺寸增加或减少时触发等。 - 兼容性和浏览器支持
ResizeObserver
是W3C
规范定义的标准API
,得到了现代浏览器的支持,包括Chrome
、Firefox
、Safari
、Edge
等。尽管在一些旧版本的浏览器中不被支持,但可以使用polyfill
或第三方库来提供兼容性支持。 - 动态响应式布局
由于
ResizeObserver
可以精确监听元素的尺寸变化,它在实现动态响应式布局时非常有用。开发者可以根据元素尺寸的变化,实时调整布局或重新计算样式,从而实现更好的用户体验。ResizeObserver
的出现填补了在原生JavaScript
中监听元素尺寸变化的空白,为开发者提供了更好的解决方案,使得元素尺寸变化的检测更加准确、高效,从而改善了响应式布局的开发体验
最后
转载自:https://juejin.cn/post/7248832185808175141