如何用Resize Observer监听DOM元素尺寸变化?

Resize Observer 提供高性能、精确的DOM元素尺寸监听,相比 window.onresize 具有更优的性能、细粒度控制和避免循环触发的优势,适用于自适应组件、响应式布局等场景,并需注意兼容性处理与内存管理。

如何用Resize Observer监听DOM元素尺寸变化?

Resize Observer

是一个非常实用的Web API,它允许我们以高性能、非侵入性的方式,实时监听并响应DOM元素内容区域(content-box)尺寸的变化。这就像给一个元素装上了一个灵敏的传感器,一旦它的宽或高发生改变,我们就能立即得到通知,从而执行相应的逻辑,而无需依赖传统的

window.onresize

全局事件或耗费性能的轮询。

解决方案

使用

Resize Observer

的核心流程其实挺直观的,我个人觉得它比

Mutation Observer

好理解多了。

首先,你需要创建一个

ResizeObserver

实例,并传入一个回调函数。这个回调函数会在被监听元素的尺寸发生变化时被触发。

// 1. 创建 ResizeObserver 实例 const myObserver = new ResizeObserver(entries => {   // entries 是一个 ResizeObserverEntry 对象的数组   // 每一个 entry 都代表一个被监听元素的尺寸变化   for (let entry of entries) {     // entry.target 是发生尺寸变化的 DOM 元素     // entry.contentRect 提供了该元素新的 content-box 尺寸(DOMRectReadOnly 对象)     // entry.contentBoxSize 和 entry.borderBoxSize 提供了更详细的尺寸信息(ResizeObserverSize 数组)     // 它们提供了更精确的 width 和 height,考虑了设备的像素比      const { width, height } = entry.contentRect; // 常用     // 或者更现代的写法,获取最新的 content-box 尺寸     // const latestContentBoxSize = entry.contentBoxSize[0]; // 数组,通常只有一个元素     // const width = latestContentBoxSize.inlineSize;     // const height = latestContentBoxSize.blockSize;      console.log(`元素 ${entry.target.id || entry.target.tagName} 的新尺寸是:${width}x${height}`);      // 在这里执行你希望在尺寸变化时触发的逻辑     // 例如:重新绘制图表、调整组件布局、更新文本溢出状态等     if (entry.target.id === 'myChartContainer') {       updateChart(entry.target, width, height);     }   } });  // 2. 选择你要监听的 DOM 元素 const chartContainer = document.getElementById('myChartContainer'); const sidebar = document.getElementById('sidebar');  // 3. 开始监听这些元素 if (chartContainer) {   myObserver.observe(chartContainer); } if (sidebar) {   myObserver.observe(sidebar); }  // 4. 当不再需要监听时,记得取消监听,防止内存泄漏 // 例如,在一个组件销毁时调用: // myObserver.unobserve(chartContainer); // 取消监听单个元素 // myObserver.disconnect(); // 取消监听所有元素,并停止观察者

这个过程的核心就是:创建观察者 -> 指定目标 -> 开始观察 -> (可选)停止观察。回调函数里的

entries

数组尤其关键,它让你能知道是哪个元素发生了变化,以及变化后的具体尺寸。

Resize Observer与传统方法(如window.onresize)相比,有哪些显著优势?

说实话,我刚开始接触前端时,遇到需要监听元素尺寸变化的需求,第一反应总是

window.onresize

,然后就是一堆手动计算和性能担忧。但

Resize Observer

的出现,简直是解决这类问题的利器,它和传统方法比起来,优势简直不要太多。

首先,最核心的优势就是性能和粒度

window.onresize

监听的是整个浏览器窗口的尺寸变化,这意味着即使你只关心页面中某个小组件的尺寸,每次用户调整浏览器窗口大小,这个全局事件都会被触发。这不仅会造成不必要的计算,还可能导致页面卡顿,尤其是在回调函数中执行了复杂DOM操作时。而

Resize Observer

则完全不同,它只监听你明确指定的DOM元素,并且浏览器对它的触发机制做了大量优化,它会在每次布局和渲染之后异步触发,通常与

requestAnimationFrame

同步,这意味着它不会阻塞主线程,而且能有效避免“布局抖动”问题。

其次,是精确性和便捷性

window.onresize

只能告诉你视口变了,至于你目标元素的尺寸,你得自己通过

getBoundingClientRect()

或者

offsetWidth

/

offsetHeight

去获取,这本身就是额外的计算。

Resize Observer

的回调直接就给你提供了

ResizeObserverEntry

对象,里面包含了

contentRect

contentBoxSize

borderBoxSize

等详细的尺寸信息。你甚至可以知道是

content-box

还是

border-box

发生了变化,这对于需要精确控制布局的场景来说,简直是福音。我记得有一次我写一个响应式图表,就是因为

Resize Observer

提供了精确的

content-box

尺寸,才让我省去了不少手动计算 padding 和 border 的麻烦。

还有一个非常重要的点,就是它避免了潜在的无限循环和循环引用问题。一个常见的误区是,在

window.onresize

的回调里改变了某个元素的尺寸,这可能会再次触发

window.onresize

,如果处理不当,就可能陷入无限循环。但

Resize Observer

在设计上就考虑到了这一点,即使你在回调中改变了被监听元素的尺寸,它也不会在同一帧内再次触发,有效防止了这种“自激”现象。这让我能更放心地在回调中进行布局调整,而不用担心副作用。

在实际项目中,Resize Observer有哪些常见的应用场景和潜在的挑战?

在我多年的开发经验里,

Resize Observer

已经成了我工具箱里不可或缺的一部分。它的应用场景非常广泛,我个人觉得主要集中在需要动态响应内部元素尺寸变化的场景。

常见的应用场景:

  1. 自适应组件或图表库: 这是最典型的应用。比如你有一个复杂的D3.js或Echarts图表,它需要根据父容器的尺寸变化来重新渲染或调整内部布局。用
    Resize Observer

    监听图表容器,一旦尺寸变了,就调用图表的

    resize()

    方法,完美!我经常用它来做仪表盘上的各种小部件,它们的大小可能因为侧边栏的展开或折叠而改变。

  2. 响应式布局和容器查询: 虽然CSS的
    @media

    查询主要针对视口,但很多时候我们希望组件能根据其父容器的尺寸变化来调整样式,这被称为“容器查询”。在原生CSS的容器查询还未普及或不满足需求时,

    Resize Observer

    就是一个很好的替代方案。我曾用它来根据父容器宽度,动态切换子元素的排列方式或字体大小。

  3. 虚拟列表和无限滚动: 当列表容器的高度变化时,我们可能需要动态计算可见区域能容纳多少个列表项,从而优化渲染性能。
    Resize Observer

    就能监听这个容器的高度,及时更新虚拟列表的渲染范围。

  4. 文本溢出处理: 某些场景下,我们可能希望文本在特定容器内显示,如果溢出就显示省略号,或者动态调整字体大小。
    Resize Observer

    可以帮助我们监听容器尺寸,然后判断文本是否溢出,并执行相应的处理。

  5. 拖拽/缩放组件: 如果你的应用允许用户拖拽或缩放某个UI组件(例如一个可调整大小的面板),
    Resize Observer

    可以监听这个组件的尺寸变化,然后更新其内部内容或通知其他依赖组件。

潜在的挑战:

  1. 兼容性问题: 这是老生常谈了,虽然现代浏览器支持度很好,但IE系列是完全不支持的。这意味着在需要兼容旧版浏览器的项目中,你可能需要引入 Polyfill。
  2. 回调触发时机和性能考量: 尽管
    Resize Observer

    自身性能优秀,但如果你的回调函数中执行了非常耗时或大量的DOM操作,仍然可能导致性能问题。理解它是在布局和渲染之后异步触发的,有助于你更好地安排回调内的逻辑,比如将复杂的DOM修改放到

    requestAnimationFrame

    中。

  3. 内存管理: 忘记
    unobserve()

    disconnect()

    是一个常见的坑,尤其是在单页应用(SPA)中。当组件被销毁时,如果其内部创建的

    Resize Observer

    没有被清理,它会继续监听已经不存在的DOM元素,导致内存泄漏。我常常会遇到这种情况,然后得花点时间去排查。

  4. 嵌套监听的复杂性: 如果你同时监听了父元素和子元素,当父元素尺寸变化时,子元素的尺寸也可能随之变化,这会导致多个
    Resize Observer

    回调被触发。虽然这通常不是问题,但在某些复杂场景下,可能会让调试变得稍微复杂一点。

如何处理Resize Observer的兼容性问题,以及在回调中如何避免性能陷阱?

处理

Resize Observer

的兼容性和优化其性能,是把它用好、用稳的关键。

兼容性解决方案:

如何用Resize Observer监听DOM元素尺寸变化?

Snyk Code

当下比较流行的代码安全检查工具

如何用Resize Observer监听DOM元素尺寸变化?27

查看详情 如何用Resize Observer监听DOM元素尺寸变化?

最直接有效的方式就是使用 Polyfill。市面上有一些成熟的

Resize Observer

Polyfill 库,比如

github.com/que-etc/resize-observer-polyfill

  1. 安装 Polyfill: 如果你使用npm或yarn,可以这样安装:

    npm install resize-observer-polyfill # 或者 yarn add resize-observer-polyfill
  2. 引入和使用: 在你的应用入口文件或需要使用

    Resize Observer

    的地方引入它。通常,你只需要在全局环境下确保

    window.ResizeObserver

    存在即可。

    import ResizeObserver from 'resize-observer-polyfill';  // 确保全局的 ResizeObserver 可用 if (!window.ResizeObserver) {   window.ResizeObserver = ResizeObserver; }  // 现在你就可以放心地使用 new ResizeObserver(...) 了

    当然,你也可以在条件判断后,只在不支持的浏览器中加载这个 Polyfill,实现按需加载,进一步优化性能。

  3. 特性检测: 在实际使用

    new ResizeObserver()

    之前,最好进行一个简单的特性检测,虽然引入 Polyfill 后这步可能显得不那么必要,但它仍是一个良好的编程习惯。

    if (typeof window.ResizeObserver !== 'undefined') {   // 使用 ResizeObserver } else {   // 提供降级方案或使用 Polyfill }

在回调中避免性能陷阱:

尽管

Resize Observer

本身是高性能的,但回调函数中的操作仍然可能成为瓶颈。

  1. 保持回调函数轻量化: 回调函数的核心职责应该是获取最新的尺寸信息,并触发必要的更新。避免在回调中执行大量、复杂的DOM操作或耗时的计算。如果确实需要执行这些操作,可以考虑将它们延迟。

  2. 利用

    requestAnimationFrame

    优化DOM操作: 如果回调中需要修改DOM,为了避免布局抖动和提高渲染效率,我通常会把这些DOM操作包裹在

    requestAnimationFrame

    中。这样可以确保你的DOM修改在浏览器下一次重绘之前统一执行,减少不必要的强制回流和重绘。

    const myObserver = new ResizeObserver(entries => {   for (let entry of entries) {     // 获取尺寸信息     const { width, height } = entry.contentRect;      // 将复杂的DOM操作或渲染逻辑放入 requestAnimationFrame     window.requestAnimationFrame(() => {       // 例如,更新一个 canvas 的尺寸并重新绘制       if (entry.target.id === 'myChartCanvas') {         const canvas = entry.target;         canvas.width = width;         canvas.height = height;         drawChart(canvas, width, height);       }     });   } });
  3. 合理使用防抖(Debounce)或节流(Throttle):

    Resize Observer

    内部已经有了一些防抖机制,它会在每一帧的末尾批量处理所有观察到的变化。但在某些极端情况下,如果你的回调函数确实非常耗时,并且你对响应的实时性要求不是那么高,你仍然可以在回调内部手动添加防抖或节流。但请注意,这可能会引入额外的延迟,所以要慎重评估。我个人经验是,大部分情况下

    Resize Observer

    自身的优化已经足够,很少需要手动加防抖。

  4. 及时清理观察者: 这是我之前提过的,但真的非常重要。当被监听的元素从DOM中移除,或者包含

    Resize Observer

    的组件被销毁时,务必调用

    observer.unobserve(element)

    来取消对特定元素的监听,或者调用

    observer.disconnect()

    来停止观察所有元素。这能有效防止内存泄漏,确保应用长期运行的稳定性。

    // 在组件卸载或元素被移除时调用 myObserver.unobserve(chartContainer); // 针对特定元素 // 或者 // myObserver.disconnect(); // 如果这个观察者只服务于这个组件,并且组件要被销载了

通过这些方法,你就能在项目中更稳健、更高效地利用

Resize Observer

,为用户带来更流畅的交互体验。

css js 前端 git github 浏览器 回调函数 工具 ai echarts win 响应式布局 排列 css echarts npm yarn 回调函数 循环 线程 主线程 JS 对象 事件 dom 异步 padding border github 传感器 ui

上一篇
下一篇