以前要做“当某个div变窄就隐藏侧栏”这种效果,只能往window上挂resize事件,然后读取元素offsetWidth来判断。但这样有一个问题:如果元素是因为其他元素变化而被撑宽的,window尺寸没变,resize事件根本不会触发。后来有了ResizeObserver,终于可以直接观察任意元素的尺寸变化,用起来比想象中还简单。
什么时候需要它
最典型的是网页里的可拖拽侧边栏。用户拖动分隔条时,侧边栏的宽度实时变化,旁边的图表需要跟着重新布局。你再也不用自己写一个拖拽监听然后计算宽度了,只需要监听侧边栏的尺寸变化,然后执行你的重绘逻辑。
还有自适应组件:一个卡片组件,根据它的宽度决定是横排还是竖排。用ResizeObserver就能完美实现“容器查询”的效果,类似于未来的CSS container queries,但现在就可以用。
基础用法
ResizeObserver 的API异常简单,就三个方法:observe、unobserve、disconnect。
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
// entry.target 是被观察的元素
// entry.contentRect 是元素的尺寸信息,包括宽度、高度
console.log('元素变了', entry.contentRect.width, entry.contentRect.height);
}
});
// 开始观察
observer.observe(document.getElementById('main'));
// 停止观察某一个
observer.unobserve(document.getElementById('main'));
// 取消所有观察
observer.disconnect();
注意回调里传给你的是一个数组,数组里每个项对应一个被观察的元素。即使只观察一个,也要习惯用for循环,因为以后可能多绑定几个。
contentRect 是什么
entry.contentRect 包含 width、height、top、right、bottom、left 和 x、y。注意这里宽高是内容盒(content box)的尺寸,不包括padding和border。如果你想获取border box的尺寸,其实在回调里直接用 entry.target.getBoundingClientRect() 也行。
有一点需要注意:回调里拿到的尺寸是“变化后”的值,你无法直接获取上一个尺寸。如果你要判断是变宽还是变窄,得自己在外部保存上一次的值。
实战:做一个拖拽侧栏,并实时更新主区域文字大小
我写一个编辑器布局:左边是侧边栏,可以拖拽调整宽度,右边内容区根据宽度变化显示不同字号。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>ResizeObserver 拖拽分区</title>
</head>
<body>
<div id="app" style="display: flex; height: 100vh;">
<div id="sidebar" style="width: 200px; background: #f0f0f0; min-width: 100px;">侧边栏</div>
<div id="divider" style="width: 5px; background: #ccc; cursor: col-resize;"></div>
<div id="content" style="flex: 1; padding: 20px; overflow: hidden;">
<div id="text">内容区域</div>
</div>
</div>
<script>
const sidebar = document.getElementById('sidebar');
const divider = document.getElementById('divider');
// 拖拽调整宽度功能
divider.addEventListener('mousedown', (e) => {
e.preventDefault();
const startX = e.clientX;
const startWidth = sidebar.getBoundingClientRect().width;
function onMouseMove(e) {
const delta = e.clientX - startX;
const newWidth = startWidth + delta;
// 限制最大最小宽度
sidebar.style.width = Math.max(100, Math.min(500, newWidth)) + 'px';
}
function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
// 监听侧边栏宽度变化,调整内容字号
const content = document.getElementById('text');
const observer = new ResizeObserver(entries => {
const sidebarWidth = entries[0].contentRect.width;
// 根据宽度值设定字号
if (sidebarWidth < 150) {
content.style.fontSize = '12px';
} else if (sidebarWidth < 250) {
content.style.fontSize = '16px';
} else {
content.style.fontSize = '20px';
}
// 顺便打印出来方便观察
console.log('侧边栏宽度:', sidebarWidth);
});
observer.observe(sidebar);
</script>
</body>
</html>
运行后拖动中间的条,侧边栏宽度一变,右侧文字字号自动切换。整个过程中你没有依赖window resize,这个效果在元素被其他方式改变尺寸时同样会触发。
监听元素出现在视口时的场景
其实还有一个和它很像的API叫IntersectionObserver,用来检测元素是否在视口中,很多人会把它们弄混。ResizeObserver管尺寸变化,IntersectionObserver管可见性变化。两者可以配合用,比如当某个元素刚进入视口时,再开始监听它的尺寸变化。
坑一:回调会在一开始初始化时触发一次
ResizeObserver 在observe一个元素后,会立即触发一次回调,把初始尺寸告诉你。这是一个好消息,因为你不用手动再执行一次初始化逻辑。但如果你在回调里无限次修改目标元素的尺寸,可能会造成循环触发。例如你在回调里把元素的宽度加1px,那就会一直触发。所以必须确保回调逻辑有终止条件,或者不要去修改导致尺寸变化的属性。
observer.observe(document.body); // 会立即回调一次
坑二:contentRect.width 是浮点数,不是整数
比如1920.5这种,别直接用 === 比较。我在一个项目里因为精度问题导致条件判断出了bug。你用比较宽松的范围判断,或者用Math.round处理。
坑三:在隐藏元素上观察,可能拿不到预期值
如果元素的 display: none,它的尺寸是0。ResizeObserver依然会触发回调,但contentRect.width和height是0。所以如果元素需要在隐藏后显示,显示时可能需要重新触发。有个小技巧:用 visibility: hidden 代替 display: none,这样尺寸还在。
坑四:多设备像素比
在Retina屏幕上,contentRect.width是CSS像素,不是物理像素。你不需要做额外处理,但如果要用它绘制canvas,最好不要直接用这个值,而要考虑乘以devicePixelRatio。
实现一个类似CSS容器查询的“宽高比”组件
现在很多UI组件库都有了“响应式布局”,用resize-observer就是实现内部机制。这里我写一个小demo:一个div,当宽度大于高度时,背景变浅色,当高度大于宽度时,背景变深色。
<div id="box" style="width: 200px; height: 150px; transition: background 0.2s;">调整我</div>
<button onclick="swap()">切换宽高</button>
<script>
const box = document.getElementById('box');
function swap() {
// 交换宽高
const w = box.offsetWidth;
const h = box.offsetHeight;
box.style.width = h + 'px';
box.style.height = w + 'px';
}
const ob = new ResizeObserver(entries => {
const { width, height } = entries[0].contentRect;
if (width > height) {
box.style.background = '#e0e0e0';
} else if (height > width) {
box.style.background = '#90caf9';
} else {
box.style.background = '#fff';
}
});
ob.observe(box);
</script>
点按钮交换宽高,背景色自动切换,不需要手动更新样式。
在框架中使用:React/Vue
如果你是Vue3,在 setup 里使用 ref 注册元素,然后在 onMounted 里创建observer:
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
const boxRef = ref(null);
let observer;
onMounted(() => {
observer = new ResizeObserver(() => {
console.log('尺寸变了');
});
observer.observe(boxRef.value);
});
onBeforeUnmount(() => {
observer.disconnect();
});
</script>
React 里面也在 useEffect 里做类似操作。但注意在React 18严格模式下,useEffect会执行两次,所以记得先清理上一次的observer再创建新的。
性能会不会很差
ResizeObserver 和 IntersectionObserver 一样,都是浏览器原生实现,回调是异步触发的(微任务)。它不会像scroll事件那样频繁触发,所以性能比我预期的好。但如果在回调里做复杂操作,比如重排、重绘,一样会卡。建议在回调里用requestAnimationFrame合并一下多次变化:
let ticking = false;
observer = new ResizeObserver(entries => {
if (!ticking) {
requestAnimationFrame(() => {
// 真正更新
updateLayout(entries);
ticking = false;
});
ticking = true;
}
});
兼容性
现在主流浏览器都支持,包括移动端。IE早就不管了,所以放心用。如果你要兼容非常老的环境,可以引入polyfill,不过现在基本没必要。
总结
ResizeObserver 是一个很不起眼但极其好用的API。它让“容器尺寸变化”这个监听逻辑变得简单安全。以前为了做自适应图表,我得频繁检查window resize,还得重新计算多个容器的尺寸。现在一个observer观察多个元素,回调里统一处理,代码减了一大半。如果你还没用过它,下次遇到“这个div宽度变了”的需求,记得第一时间想起它。

