远古方案:resize的弊端
-
全局监听,粒度粗糙
window.resize是全局事件,任何窗口尺寸变化都会触发回调,即使页面中目标元素并未发生实际变化。开发者需手动遍历并比对所有关注元素的尺寸,逻辑冗余且效率低下。 -
频繁触发,易导致性能瓶颈
用户拖拽调整窗口时,
resize事件会高频连续触发,若回调中包含复杂计算或 DOM 操作,极易引发页面卡顿。虽可通过节流(throttle)优化,但仍无法从根本上解决问题。 -
无法感知非窗口引起的元素变化
当元素因内容更新、CSS 动画、父容器结构调整等非窗口 resize 行为导致尺寸变化时,该事件完全无法捕获,存在严重的监听盲区。
下面这段代码,是旧版本的vben-admin封装的echarts的hooks,使用了监听window.resize的方式监听屏幕尺寸变化,从而去重新渲染echarts图表:
js
function initCharts(t = theme) {
const el = unref(elRef);
if (!el || !unref(el)) {
return;
}
chartInstance = echarts.init(el, t);
const { removeEvent } = useEventListener({
el: window,
name: 'resize',
listener: resizeFn,
});
removeResizeFn = removeEvent;
const { widthRef, screenEnum } = useBreakpoint();
if (unref(widthRef) <= screenEnum.MD || el.offsetHeight === 0) {
useTimeoutFn(() => {
resizeFn();
}, 30);
}
}
可见大部分人的第一想法还是用resize这个深入人心的方案。
完善的第三方库------element-resize-detector
element-resize-detector 的底层原理是通过在目标元素内部创建隐藏的 <object> 或监听滚动事件来间接检测尺寸变化,从而规避浏览器无法原生监听元素 resize 的限制。
使用方式:
js
import elementResizeDetectorMaker from "element-resize-detector";
const erd = elementResizeDetectorMaker();
const chartContainer = document.getElementById("chartContainer");
// 模拟图表绘制函数
function renderChart(width, height) {
console.log(`重新绘制图表:宽${width}px,高${height}px`);
// 实际项目中此处可调用ECharts、HighCharts等图表库的重绘方法
}
// 监听元素尺寸变化 回调函数里重绘
erd.listenTo(chartContainer, function(element) {
const width = element.offsetWidth;
const height = element.offsetHeight;
renderChart(width, height);
})
具体实现依赖两种核心策略:
- 滚动(scroll)策略(默认)
在目标元素内动态插入一个隐藏的<div>,并设置其宽高略大于父元素,同时监听该元素的scroll事件。当目标元素尺寸发生变化时,内部元素的可滚动区域随之改变,从而触发scroll事件,实现对尺寸变化的捕获。该方式性能高、兼容性好,适用于绝大多数现代浏览器。
具体步骤:
-
初始化监听事件和初始的尺寸
-
注入滚动元素
-
注册监听:
registerListenersAndPositionElements里监听了渲染、收缩、扩展事件jsgetState(element).onRendered = handleRender; getState(element).onExpand = handleScroll; getState(element).onShrink = handleScroll;jsfunction install() { initListeners(); storeStartSize(); batchProcessor.add(0, storeStyle); batchProcessor.add(1, injectScrollElements); batchProcessor.add(2, registerListenersAndPositionElements); batchProcessor.add(3, finalizeDomMutation); batchProcessor.add(4, ready); }
- 对象(object)策略
将一个隐藏的<object>标签嵌入目标元素内部,并指向当前页面。<object>元素会自动填充其容器,当容器尺寸变化时,<object>会触发自身的resize事件。由于<object>支持监听resize,因此可借此间接感知父元素的尺寸变化。此方法主要用于兼容老旧浏览器(如 IE)。

核心逻辑:
js
// IE8 直接给元素绑定resize事件
if(browserDetector.isIE(8)) {
//IE 8 does not support object, but supports the resize event directly on elements.
getState(element).object = {
proxy: listenerProxy
};
element.attachEvent("onresize", listenerProxy);
} else {
var object = getObject(element);
if(!object) {
throw new Error("Element is not detectable by this strategy.");
}
object.contentDocument.defaultView.addEventListener("resize", listenerProxy);
}
对比:
| 策略名称 | 优点 | 缺点 |
|---|---|---|
| object | 精确性高 | 性能相对差,尤其是同一个页面有多个元素需要监听时 |
| scroll | 性能较好 | 精确性较低、特定布局无法使用(源码中有大量的兼容性处理) |
现代浏览器的处理方案------resizeObserver------以及简单的封装
ResizeObserver 是现代浏览器提供的原生 API,用于监听 DOM 元素内容区域的尺寸变化,无需依赖轮询或事件冒泡,具有高精度、高性能的特点。
-
精准监听元素级尺寸变化
ResizeObserver 能直接监听任意 DOM 元素的
content-box或border-box尺寸变化,包括由 CSS 动画、内容更新、布局重排等引起的改变,无需手动计算或依赖窗口 resize 事件。 -
异步回调,避免性能瓶颈
回调函数通过
requestAnimationFrame异步执行,不会阻塞渲染线程,即使在高频变化场景下也能保持页面流畅。 -
自动忽略不可见元素
对于
display: none或未插入文档的元素,ResizeObserver 会自动暂停监听,减少无效计算。 -
支持批量处理与资源清理
observe(element):开始监听指定元素。unobserve(element):停止监听某个元素。disconnect():断开所有监听,释放资源。
使用方式:
js
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const { width, height } = entry.contentRect; // content-box 尺寸
console.log(`内容区域尺寸:${width}x${height}`);
});
});
// 开始监听
observer.observe(document.getElementById('target'));
兼容性问题:
resizeObserver的浏览器支持情况如下: Chrome: 从版本 64 开始支持(2018 年 1 月发布) Firefox: 从版本 69 开始支持(2019 年 9 月发布) Edge: 从版本 79 开始支持(2020 年 1 月发布)。 Safari: 从版本 13.1 开始支持(2020 年 3 月发布)。 Opera: 从版本 51 开始支持(2018 年 1 月发布)。
若需要支持更低版本的浏览器,需要引用polyfill文件。
简单的封装:
js
import ResizeObserver from 'resize-observer-polyfill';
function resizeHandler(entries: any[]) {
for (const entry of entries) {
const listeners = entry.target.__resizeListeners__ || [];
if (listeners.length) {
listeners.forEach((fn: () => any) => {
fn();
});
}
}
}
export function addResizeListener(element: any, fn: () => any) {
if (!element.__resizeListeners__) {
element.__resizeListeners__ = [];
element.__ro__ = new ResizeObserver(resizeHandler);
element.__ro__.observe(element);
}
element.__resizeListeners__.push(fn);
}
export function removeResizeListener(element: any, fn: () => any) {
if (!element || !element.__resizeListeners__) return;
element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1);
if (!element.__resizeListeners__.length) {
element.__ro__.disconnect();
}
}
上面的封装是为了处理同一个元素多次监听,避免造成监听的覆盖。封装之后,用法和addEventListener比较相似,只是无需传入事件类型。
使用方式:
js
const chartContainer = document.getElementById("chartContainer");
// 模拟图表绘制函数
function renderChart() {
console.log(`重新绘制图表`);
// 实际项目中此处可调用ECharts、HighCharts等图表库的重绘方法
}
// 监听元素尺寸变化 回调函数里重绘
addResizeListener(chartContainer, function() {
renderChart()
})