IntersectionObserver

35 阅读8分钟

一、IntersectionObserver 是什么?

一句话概括:IntersectionObserver (交叉观察器) 是一个现代浏览器 API,它提供了一种异步、高效的方式来检测一个目标元素(target)与祖先元素或顶级文档视窗(viewport)的交叉状态。

简单来说,你可以用它来回答这个问题:“我的某个元素现在是否在屏幕上可见?” 或者 “它可见了多少?”

二、它解决了什么问题?(为什么需要它)

IntersectionObserver 出现之前,要实现类似的功能,我们通常依赖于监听 scroll 事件,并在事件回调中调用 Element.getBoundingClientRect() 来计算元素的位置。

这种传统方法有两大致命缺陷

  1. 性能极差

    • scroll 事件触发非常频繁,在用户滚动页面时会连续不断地执行回调函数,这会消耗大量计算资源。
    • getBoundingClientRect() 是一个同步操作,会强制浏览器重新计算页面布局(reflow/layout),频繁调用会导致页面卡顿、掉帧,严重影响用户体验。
  2. 代码复杂

    • 你需要手动计算元素的位置、视窗的大小、滚动偏移量等,代码逻辑繁琐且容易出错。
    • 如果滚动的容器不是整个页面,而是某个可滚动的 div,计算会更加复杂。

IntersectionObserver 的出现就是为了用一种更高效、更声明式的方式解决这些问题。它将这些复杂的计算交给了浏览器本身,在后台线程中进行优化处理,完全不会阻塞主线程,从而保证了页面的流畅性。


三、核心概念与用法

使用 IntersectionObserver 主要分为三步:

  1. 创建观察器new IntersectionObserver(callback, options)
  2. 指定观察目标observer.observe(targetElement)
  3. 处理交叉状态变化:在 callback 函数中编写逻辑

1. 创建观察器 (The Constructor)

let options = {
  root: null,
  rootMargin: '0px',
  threshold: 0
};

let observer = new IntersectionObserver(callback, options);

构造函数接收两个参数:

  • callback: 当目标元素的可见性发生变化时,这个回调函数就会被触发。
  • options (可选): 一个配置对象,用来定制观察器的行为。
options 对象详解:
  • root:

    • 作用:指定哪个元素作为“视窗”或“容器”来与目标元素进行交叉检测。
    • 默认值null,表示使用浏览器的顶级视窗 (viewport)。
    • 用法:你可以传入一个具体的 DOM 元素,例如一个可滚动的 div。这样,观察器就会检测目标元素是否进入了这个 div 的可见区域。注意:root 元素必须是目标元素的祖先元素。
  • rootMargin:

    • 作用:用来扩展或收缩 root 元素的边界框(bounding box),类似于 CSS 的 margin。它允许你在目标元素实际进入视窗 之前之后 触发回调。
    • 默认值'0px'
    • 用法:值是一个字符串,格式和 CSS margin 属性一样,如 '10px 20px 30px 40px' (top, right, bottom, left)。
      • 正值:扩展 root 边界。例如 rootMargin: '100px' 会让视窗在四个方向上都向外“扩大”100px,目标元素在距离视窗还有100px时就会被视为“交叉”。常用于预加载。
      • 负值:收缩 root 边界。例如 rootMargin: '-50px' 会让视窗在四个方向上都向内“缩小”50px,目标元素必须进入这个更小的区域才算交叉。
  • threshold (阈值):

    • 作用:决定了目标元素与 root 交叉比例达到多少时触发 callback
    • 默认值0,表示目标元素的任何一个像素进入视窗,回调就会触发。
    • 用法:可以是一个数字,也可以是一个数字数组。
      • 单个数字threshold: 0.5 表示当目标元素至少有 50% 可见时,触发回调。threshold: 1 表示目标元素必须完全可见时才触发。
      • 数组threshold: [0, 0.25, 0.5, 0.75, 1] 表示每当目标元素的可见比例穿过 0%, 25%, 50%, 75%, 100% 这些点时,都会触发回调。这对于实现复杂的动画或数据上报非常有用。

2. 回调函数 (The callback)

回调函数会在观察器初始化时以及每次目标元素的交叉状态变化时被调用。

function callback(entries, observer) {
  entries.forEach(entry => {
    // 每个 entry 描述了一个被观察元素的状态变化
    if (entry.isIntersecting) {
      // 元素进入了视窗
      console.log('元素可见了:', entry.target);
    } else {
      // 元素离开了视窗
      console.log('元素不可见了:', entry.target);
    }
  });
}

回调函数接收两个参数:

  • entries: 一个 IntersectionObserverEntry 对象的数组。即使你只观察一个元素,它也是一个数组。每个 entry 对象包含了单个目标元素在交叉状态变化时的详细信息。
  • observer: 对创建的观察器实例的引用。你可以在回调内部用它来停止观察某个元素等操作。
IntersectionObserverEntry 对象详解:

entry 对象是回调函数中最重要的信息来源,它包含以下关键属性:

  • entry.isIntersecting: 最常用的属性。一个布尔值,true 表示目标元素当前至少有一部分与 root 交叉(可见),false 则表示完全分离(不可见)。
  • entry.intersectionRatio: 一个 0.0 到 1.0 之间的数字,表示目标元素的可见比例。0 表示完全不可见,1 表示完全可见。
  • entry.target: 被观察的目标 DOM 元素。
  • entry.boundingClientRect: 目标元素的矩形区域信息,等同于 target.getBoundingClientRect()
  • entry.intersectionRect: 目标元素与视窗交叉的矩形区域信息。
  • entry.rootBounds: root 元素的矩形区域信息。

3. 观察器的方法

创建了观察器实例后,它有几个重要的方法:

  • observer.observe(targetElement): 开始观察一个目标元素。
  • observer.unobserve(targetElement): 停止观察一个特定的目标元素。这是一个非常重要的性能优化点,当一个元素的任务完成后(如图片已加载),就应该停止观察它。
  • observer.disconnect(): 停止观察所有目标元素。当组件卸载或不再需要观察器时调用,以释放资源。
  • observer.takeRecords(): 返回一个包含所有待处理的 IntersectionObserverEntry 对象的数组,并清空待处理队列。

四、实战案例:图片懒加载

这是 IntersectionObserver 最经典的应用场景。

HTML 结构: 我们将真实的图片 URL 放在 data-src 属性中,src 属性放一个占位符(比如一个很小的 loading GIF 或透明图片)。

<div class="container">
  <h2>Scroll down to see images lazy-load</h2>
  <img class="lazy-image" src="placeholder.gif" data-src="https://picsum.photos/id/101/400/300" alt="Image 1">
  <img class="lazy-image" src="placeholder.gif" data-src="https://picsum.photos/id/102/400/300" alt="Image 2">
  <img class="lazy-image" src="placeholder.gif" data-src="https://picsum.photos/id/103/400/300" alt="Image 3">
  <!-- ... 更多图片 ... -->
  <img class="lazy-image" src="placeholder.gif" data-src="https://picsum.photos/id/120/400/300" alt="Image 20">
</div>

CSS 样式: 给图片一些间距,确保页面可以滚动。

.container {
  width: 80%;
  margin: 0 auto;
}
.lazy-image {
  display: block;
  width: 400px;
  height: 300px;
  margin: 20px 0;
  background-color: #f0f0f0;
  border: 1px solid #ccc;
}

JavaScript 实现:

document.addEventListener("DOMContentLoaded", () => {
  // 1. 选中所有需要懒加载的图片
  const lazyImages = document.querySelectorAll('.lazy-image');

  // 2. 定义回调函数
  const imageObserverCallback = (entries, observer) => {
    entries.forEach(entry => {
      // 如果元素进入视窗
      if (entry.isIntersecting) {
        const image = entry.target;
        
        // 将 data-src 的值赋给 src
        image.src = image.dataset.src;
        
        // 可选:移除 class,防止重复加载
        image.classList.remove('lazy-image');
        
        // 3. 关键:图片加载后,停止观察该图片,以提升性能
        observer.unobserve(image);
      }
    });
  };

  // 4. 配置 options
  const observerOptions = {
    // root: null, // 默认是 viewport
    rootMargin: '0px 0px 100px 0px', // 提前 100px 开始加载
    // threshold: 0.01 // 只要有 1% 进入就触发
  };

  // 5. 创建观察器实例
  const imageObserver = new IntersectionObserver(imageObserverCallback, observerOptions);

  // 6. 让观察器观察所有图片
  lazyImages.forEach(image => {
    imageObserver.observe(image);
  });
});

代码解析:

  1. 我们选中所有带有 .lazy-image 类的图片。
  2. 在回调函数中,我们检查 entry.isIntersecting。如果为 true,说明图片进入了我们的观察区域。
  3. 我们将 data-src 的真实 URL 赋值给 src,浏览器就会开始加载图片。
  4. 最重要的一步:我们调用 observer.unobserve(image) 来停止对这张图片的观察,因为它已经完成了任务,没必要再浪费资源去监视它。
  5. options 中,我们设置了 rootMargin: '0px 0px 100px 0px',这意味着当图片距离视窗底部还有 100px 时,就会被认为是“可见的”,从而实现提前加载,用户体验更好。

五、其他常见用例

  • 无限滚动:在列表底部放置一个“哨兵”元素,当这个哨兵元素进入视窗时,就加载下一页数据。
  • 触发动画:当元素滚动到可视区域时,为其添加一个 CSS 动画类。
  • 广告曝光统计:当广告元素的可见比例(intersectionRatio)超过 0.5 且持续一定时间后,记录一次有效的广告曝光。
  • 视频自动播放/暂停:当视频进入视窗时自动播放,离开时自动暂停。

六、浏览器兼容性

IntersectionObserver 是一个现代 API,主流浏览器(Chrome, Firefox, Safari, Edge)都已支持。对于旧版浏览器(如 IE),你需要使用 polyfill。

总结: IntersectionObserver 是一个强大、高性能的前端 API,它彻底改变了我们处理滚动相关交互的方式。通过将复杂的可见性检测交给浏览器,我们能以极低的性能成本实现懒加载、无限滚动等常见功能,是现代 Web 开发必备的利器。