ResizeObserver 实战指南 轻松搞定元素尺寸监听的那些坑
我的写法,亲测靠谱
用ResizeObserver这么久,我发现很多人还是在瞎写。我先说说我现在固定用的写法:
class ResponsiveElement {
constructor(element) {
this.element = element;
this.resizeObserver = null;
this.currentSize = null;
this.callbacks = [];
}
init() {
this.resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const rect = entry.contentRect;
// 防抖处理,避免频繁触发
if (this.currentSize &&
Math.abs(rect.width - this.currentSize.width) < 1 &&
Math.abs(rect.height - this.currentSize.height) < 1) {
return; // 尺寸变化太小,忽略
}
this.currentSize = { width: rect.width, height: rect.height };
// 执行所有回调
this.callbacks.forEach(callback => callback({
width: rect.width,
height: rect.height,
target: entry.target
}));
}
});
this.resizeObserver.observe(this.element);
}
onResize(callback) {
this.callbacks.push(callback);
}
destroy() {
if (this.resizeObserver) {
this.resizeObserver.unobserve(this.element);
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
this.callbacks = [];
}
}
// 使用示例
const responsiveBox = new ResponsiveElement(document.querySelector('.responsive-box'));
responsiveBox.init();
responsiveBox.onResize(({width, height}) => {
console.log(当前尺寸: ${width} x ${height});
});
这种写法的好处很明显:封装性强、内存泄漏风险低、防抖机制避免频繁触发。我之前就是这么给公司项目重构的,效果还不错。
这里要注意几个点:首先contentRect获取的是元素内容区域的尺寸,borderBoxSize才是包含边框的。大部分场景下用contentRect就够了,除非你要精确计算包含边框的整体大小。
这几种错误写法,别再踩坑了
我见过太多错误用法了,列几个典型的:
错误写法一:直接用箭头函数,不保存实例引用
// 错误!会导致无法断开观察
new ResizeObserver(() => {
// 业务逻辑
}).observe(element);
// 这样就完了,想unobserve都没办法,内存泄漏预警
错误写法二:频繁创建和销毁观察器
function handleResize() {
const observer = new ResizeObserver(() => {
// 某些逻辑
});
observer.observe(element); // 每次都重新创建
// 想着马上断开
setTimeout(() => {
observer.disconnect(); // 但其实可能还在执行回调
}, 0);
}
这种写法简直就是灾难,不仅性能差,还容易出现竞态条件。我之前维护的一个项目就是这样的,页面卡得要死。
错误写法三:在回调里直接修改被观察元素的尺寸
const observer = new ResizeObserver((entries) => {
for (let entry of entries) {
// 直接修改尺寸,会导致无限循环
entry.target.style.width = entry.contentRect.width + 10 + 'px';
}
});
这种情况会出现无限resize循环,浏览器直接卡死。我第一次遇到这个问题时,调试了整整一个下午。
实际项目中的坑
实际项目用的时候,有几个特别需要注意的地方。
第一个坑:元素隐藏时的处理
当元素display:none时,ResizeObserver不会触发回调。如果你需要监听元素显示/隐藏,还得配合其他手段:
// 结合Intersection Observer处理元素可见性
const intersectionObserver = new IntersectionObserver((entries) => {
for (let entry of entries) {
if (entry.isIntersecting) {
// 元素变为可见,重新检查尺寸
checkCurrentSize();
}
}
}, { threshold: 0 });
// 同时使用两个observer
intersectionObserver.observe(targetElement);
resizeObserver.observe(targetElement);
第二个坑:批量更新优化
如果页面有多个需要响应式调整的元素,建议统一管理:
class BatchResizeManager {
constructor() {
this.elements = new Map();
this.observer = new ResizeObserver(this.handleBatchResize.bind(this));
this.pendingUpdates = new Set();
this.frameId = null;
}
observe(element, callback) {
this.elements.set(element, callback);
this.observer.observe(element);
}
handleBatchResize(entries) {
for (let entry of entries) {
this.pendingUpdates.add(entry.target);
}
// 批量处理,避免重复重绘
if (this.frameId) {
cancelAnimationFrame(this.frameId);
}
this.frameId = requestAnimationFrame(() => {
this.processUpdates();
});
}
processUpdates() {
this.pendingUpdates.forEach(target => {
const callback = this.elements.get(target);
if (callback) {
callback(target.getBoundingClientRect());
}
});
this.pendingUpdates.clear();
}
}
这样处理可以有效减少重排次数,提升性能。我在做一个数据可视化项目时就用这套方案,几十个图表同时渲染也不会卡顿。
第三个坑:容器嵌套问题
父子元素同时监听resize时,容易出现连锁反应。我的解决方案是:
- 设置最小尺寸变化阈值,小变化直接忽略
- 给不同层级的元素设置不同的响应策略
- 必要时使用setTimeout延迟处理
性能优化要点
ResizeObserver虽然比window.resize性能好很多,但不当使用还是会出问题。
我一般会在业务层加一个简单的节流:
function createThrottledResizeHandler(handler, delay = 16) {
let timeoutId = null;
let lastExecTime = 0;
return function(...args) {
const currentTime = Date.now();
if (currentTime - lastExecTime > delay) {
handler.apply(this, args);
lastExecTime = currentTime;
} else {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
handler.apply(this, args);
lastExecTime = Date.now();
}, delay - (currentTime - lastExecTime));
}
};
}
// 使用
const throttledHandler = createThrottledResizeHandler(handleElementResize, 32);
resizeObserver.observe(element);
32ms的节流时间基本够用了,既保证了响应性又控制了性能消耗。
兼容性处理不能少
虽然现代浏览器支持度不错,但还是得考虑兼容性:
function createResponsiveElement(element, callback) {
if (typeof ResizeObserver !== 'undefined') {
// 现代浏览器
const observer = new ResizeObserver(callback);
observer.observe(element);
return observer;
} else {
// 降级方案
let currentSize = element.getBoundingClientRect();
const timer = setInterval(() => {
const newSize = element.getBoundingClientRect();
if (newSize.width !== currentSize.width || newSize.height !== currentSize.height) {
callback([{ contentRect: newSize }]);
currentSize = newSize;
}
}, 100); // 降低检测频率避免卡顿
return { disconnect: () => clearInterval(timer) };
}
}
降级方案虽然不够精确,但至少能跑起来。我一般只在需要支持老版本IE的时候才用。
以上是我关于ResizeObserver的一些实战心得,主要是踩过不少坑之后总结出来的。有些地方可能还有优化空间,但目前这套方案在我手上几个项目都运行稳定。如果你有更好的实现方式,欢迎评论区交流。

暂无评论