理解三大 Observer:IntersectionObserver、MutationObserver 和 ResizeObserver

0 阅读6分钟

在 Web 开发中,我们经常需要监听某些变化并作出响应——元素是否出现在视口中?DOM 树是否被修改?元素尺寸是否改变?

浏览器提供了三种 Observer API 来优雅地解决这些问题。

本文将全面介绍 IntersectionObserver、MutationObserver 和 ResizeObserver 的概念、用法及典型场景,并通过一个 ECharts 自适应案例,帮你彻底搞懂如何选择。

前言

过去,我们可能用 scroll 事件监听元素可见性(结合 getBoundingClientRect),用 Mutation Event 监听 DOM 变化(已被废弃),用 window.resize 事件配合 getComputedStyle 猜测元素尺寸变化。这些方式不仅性能差,而且代码复杂。

现代浏览器提供了三个专用的观察器 API:

  • IntersectionObserver:观察元素与视口(或祖先元素)的交叉状态
  • MutationObserver:观察 DOM 树的变化
  • ResizeObserver:观察元素尺寸的变化

它们都是异步的,不会阻塞主线程,且提供了更精细的配置。下面我们逐一深入。

1. IntersectionObserver:观察元素的可见性

概念

IntersectionObserver 用于异步观察目标元素与其祖先容器或顶级文档视口的交叉状态。

简单说,就是监测元素是否出现在视口中,以及出现在视口中的比例。

用法

const observer = new IntersectionObserver(callback, options);
observer.observe(targetElement);
  • callback:当交叉状态变化时调用,接收一个 entries 数组,每个 entry 包含目标的交叉信息(如 intersectionRatioisIntersecting 等)。
  • options:可配置根容器(root)、阈值(threshold)、边距(rootMargin)等。

典型场景

  • 图片懒加载:当图片元素进入视口时才开始加载真实图片
  • 无限滚动:当页面滚动到底部附近的占位元素出现时,加载更多内容
  • 曝光统计:记录广告或内容区块被用户看到的时长和次数
  • 视差滚动/动画触发:当元素进入视口时播放动画

示例:懒加载

const lazyImages = document.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img); // 加载后停止观察
    }
  });
}, { rootMargin: '50px' }); // 提前 50px 触发

lazyImages.forEach(img => observer.observe(img));

2. MutationObserver:观察 DOM 变化

概念

MutationObserver 用于监听 DOM 树的变化,包括子节点的添加/移除、属性变化、文本内容变化等。

它可以观察一个元素及其子树。

用法

const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
  • callback:接收一个 mutations 列表,每个 mutation 包含变化的类型(type)、添加/删除的节点、变化的属性等信息。
  • config:指定要观察的变化类型(必须至少指定 childListattributescharacterData 中的一个),以及是否观察子树(subtree)等。

典型场景

  • 监听动态插入的内容:例如第三方脚本往页面中添加广告,你可以监听到并对其进行样式调整
  • 实现自定义的撤销/重做:记录 DOM 变化以便回退
  • 监控 DOM 操作:调试或分析时,观察 DOM 是否被意外修改
  • 与框架集成:当 DOM 被外部修改时,同步框架状态

示例:监听属性变化

const target = document.getElementById('my-element');
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'attributes') {
      console.log(`属性 ${mutation.attributeName} 发生变化`);
    }
  });
});
observer.observe(target, { attributes: true });

3. ResizeObserver:观察元素尺寸变化

概念

ResizeObserver 用于监听元素内容区域或边框区域的大小变化。

无论变化是由窗口 resize、JavaScript 操作、CSS 布局调整还是其他原因引起的,它都能可靠地触发回调。

用法

const observer = new ResizeObserver(callback);
observer.observe(targetElement);
  • callback:接收一个 entries 数组,每个 entry 包含目标的尺寸信息(如 contentRect 包含宽度、高度等)。

典型场景

  • 响应式组件:当容器尺寸变化时,调整内部布局或重新渲染图表
  • 监听 iframe 尺寸变化:自适应 iframe 高度
  • 与第三方库集成:如 ECharts、D3.js 等需要在容器大小改变时重新绘制的库
  • 自定义滚动条或虚拟列表:根据容器尺寸计算可视区域

示例:监听容器尺寸

const container = document.getElementById('chart');
const observer = new ResizeObserver(() => {
  console.log('容器尺寸变化', container.offsetWidth, container.offsetHeight);
});
observer.observe(container);

4. 三者对比

维度IntersectionObserverMutationObserverResizeObserver
观察对象元素相对于视口/祖先的可见性DOM 树的结构、属性、文本内容元素的尺寸
触发时机当元素进入/离开视口,或交叉比例超过阈值当 DOM 节点被添加/删除、属性改变、文本内容改变当元素内容区域或边框区域尺寸改变
回调参数IntersectionObserverEntry 数组(包含交叉比例、目标矩形等信息)MutationRecord 数组(包含变化类型、新增节点、属性名等)ResizeObserverEntry 数组(包含目标尺寸信息)
主要用途滚动相关优化(懒加载、无限滚动、曝光统计)监听动态 DOM 变化、自定义撤销、调试响应式布局、图表自适应、容器尺寸监控
配置项根容器、阈值、边距子节点变化、属性变化、字符数据、子树等无(可观察多个元素)
兼容性广泛支持广泛支持现代浏览器(可 polyfill)

5. 实战案例:监听 ECharts 容器尺寸变化

假设我们有一个 ECharts 图表,希望容器尺寸改变时自动调用 chart.resize()

方案 A:IntersectionObserver ❌

// 不可行!尺寸变化不会触发交叉观察器
new IntersectionObserver(() => chart.resize()).observe(container);

IntersectionObserver 只关心可见性,尺寸变化通常不会改变可见性比例(除非变化导致元素移出视口),因此无法可靠触发。

方案 B:MutationObserver ⚠️

// 只能捕获直接修改 style 或类名的场景
new MutationObserver(() => chart.resize()).observe(container, {
  attributes: true,
  attributeFilter: ['style', 'class']
});
// 还需额外监听 window resize(如果容器随窗口变化)
window.addEventListener('resize', () => chart.resize());

MutationObserver 无法捕获由父容器布局变化引起的尺寸调整(如 flex 布局),而且需要额外处理窗口 resize,容易遗漏或过度触发。

方案 C:ResizeObserver ✅

const chart = echarts.init(container);
new ResizeObserver(() => chart.resize()).observe(container);

简洁、可靠,无论尺寸变化原因是什么,都能准确触发。

结论:对于需要监听元素尺寸变化的场景,ResizeObserver 是标准且唯一正确的选择。

总结

  • IntersectionObserver:用于元素可见性相关场景(懒加载、无限滚动、曝光统计等)
  • MutationObserver:用于监听 DOM 结构或属性变化(动态内容、自定义撤销/重做、调试等)
  • ResizeObserver:用于监听元素尺寸变化(响应式布局、图表自适应、容器尺寸监控等)

三者各司其职,没有优劣之分,只有是否适合当前需求。希望本文能帮你理清这三个 API 的区别,在合适的场景下做出正确选择。