javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JavaScript监听DOM尺寸变化

JavaScript监听DOM尺寸变化的解决方案

作者:亲亲小宝宝鸭

文章分析了传统resize监听的弊端,如粒度粗糙、性能问题等并提出了多种解决方案,通过插入<object>、监听scroll或使用现代的ResizeObserver API等来实现了更精准、高效地的元素尺寸监听,文章还讨论了这些方法的优缺点及适用场景,需要的朋友可以参考下

远古方案:resize的弊端

  1. 全局监听,粒度粗糙window.resize 是全局事件,任何窗口尺寸变化都会触发回调,即使页面中目标元素并未发生实际变化。开发者需手动遍历并比对所有关注元素的尺寸,逻辑冗余且效率低下。
  2. 频繁触发,易导致性能瓶颈‌ 用户拖拽调整窗口时,resize 事件会高频连续触发,若回调中包含复杂计算或 DOM 操作,极易引发页面卡顿。虽可通过节流(throttle)优化,但仍无法从根本上解决问题。
  3. 无法感知非窗口引起的元素变化‌ 当元素因内容更新、CSS 动画、父容器结构调整等非窗口 resize 行为导致尺寸变化时,该事件完全无法捕获,存在严重的监听盲区。

下面这段代码,是旧版本的vben-admin封装的echarts的hooks,使用了监听window.resize的方式监听屏幕尺寸变化,从而去重新渲染echarts图表:

 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 的限制。

使用方式:

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);
})

具体实现依赖两种核心策略:

  1. 滚动(scroll)策略(默认) ‌ 在目标元素内动态插入一个隐藏的 <div>,并设置其宽高略大于父元素,同时监听该元素的 scroll 事件。当目标元素尺寸发生变化时,内部元素的可滚动区域随之改变,从而触发 scroll 事件,实现对尺寸变化的捕获。该方式性能高、兼容性好,适用于绝大多数现代浏览器。

具体步骤:

getState(element).onRendered = handleRender;
getState(element).onExpand = handleScroll;
getState(element).onShrink = handleScroll;
function 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)。

核心逻辑:

// 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 会自动暂停监听,减少无效计算。

支持批量处理与资源清理

使用方式:

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文件。

简单的封装:

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比较相似,只是无需传入事件类型。

使用方式:

const chartContainer = document.getElementById("chartContainer");

// 模拟图表绘制函数
function renderChart() {
    console.log(`重新绘制图表`);
    // 实际项目中此处可调用ECharts、HighCharts等图表库的重绘方法
}

// 监听元素尺寸变化 回调函数里重绘
addResizeListener(chartContainer, function() {
    renderChart()
})

以上就是JavaScript监听DOM尺寸变化的解决方案的详细内容,更多关于JavaScript监听DOM尺寸变化的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文