ResizeObserver 实战指南 轻松搞定元素尺寸监听的那些坑

世博~ 前端 阅读 2,118
赞 18 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

用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的一些实战心得,主要是踩过不少坑之后总结出来的。有些地方可能还有优化空间,但目前这套方案在我手上几个项目都运行稳定。如果你有更好的实现方式,欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论