IntersectionObserver API

3 阅读3分钟

1. 为什么我们需要它?

  • 旧时代痛点: 以前检测元素是否进入视口,需要监听 scroll 事件 + 调用 getBoundingClientRect()。这会导致主线程频繁计算,引发页面卡顿(重排/Reflow)。
  • 新时代方案: IntersectionObserver 是浏览器提供的原生 API,它在后台线程异步运行,性能极高,不会阻塞主线程。

2. API 标准蓝图

// 1. 创建观察者 (定义"做什么"和"什么时候做")
const observer = new IntersectionObserver(callback, options);

// 2. 指定观察目标 (可以观察多个)
observer.observe(element1);
observer.observe(element2);


3. 配置对象:options (决定触发时机)

这是最容易混淆的部分,理解了这三个参数就理解了一半。

A. root (参考系)

  • 含义: 视窗的边界。元素要和谁产生交叉?
  • 默认值: null(代表浏览器可视窗口 Viewport)。
  • 特殊情况: 如果你在一个特定高度、设置了 overflow: scrolldiv 内部检测滚动,root 必须指向该 div 元素。

B. rootMargin (扩大/缩小判定范围)

  • 语法: 类似 CSS margin,如 '10px 20px 30px 40px' (上右下左)。必须带单位 (px 或 %)
  • 作用: 在视口边界周围建立一个“缓冲区”。
  • 正值 (e.g., '100px'): 扩大视口判定范围。场景:图片懒加载(在图片还没滚入视口,但在距离底部 100px 时就提前加载,用户无感知)。
  • 负值 (e.g., '-100px'): 缩小视口判定范围。场景:元素完全进入且往里走了一段才触发

C. threshold (触发阈值)

  • 含义: 目标元素可见面积 / 总面积的比例。
  • 取值: 0.0 到 1.0 之间的数字(或数组)。
  • 0 (默认):刚露头(哪怕 1px 进入)就触发。
  • 1完全展示(100% 进入)才触发。
  • 0.5:展示一半时触发。
  • [0, 0.5, 1]:在刚进入、进入一半、完全进入时分别触发(用于精细动画控制)。

4. 回调参数:entries (数据核心)

回调函数接收两个参数:(entries, observer)

⚠️ 重点:为什么是 entries (复数)? 因为 API 是异步批量处理的。如果你快速滚动,或者页面初始化时,多个被观察的元素可能在同一帧内状态发生改变,它们会被打包在这个数组里。

常用属性速查表:

属性名类型解释 (必记)
isIntersectingBoolean最常用true = 元素在视口内;false = 元素离开视口。
targetElement当前发生状态变化的那个 DOM 节点。
intersectionRatioNumber当前可见比例 (0.0 - 1.0)。
boundingClientRectRect目标元素的矩形信息 (top, left, width 等)。

5. 生命周期方法 (开关控制)

方法说明适用场景
observe(el)开始观察某个元素页面加载或组件挂载时。
unobserve(el)停止观察特定元素懒加载完成后(图片加载完就没必要再观察了)。
disconnect()关闭观察者,停止一切观察组件销毁时(防止内存泄漏)。

6. 实战代码模板 (Copy-Paste Ready)

场景一:图片懒加载 (Lazy Load)

这是最经典的用法,包含“一次性观察”的逻辑。

const lazyImageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    // 只有当元素处于可见状态时才处理
    if (entry.isIntersecting) {
      const img = entry.target;
      // 1. 替换真实图片地址
      img.src = img.dataset.src; 
      img.classList.remove('lazy');
      
      // 2. 关键:加载完了就停止观察,节省资源!
      observer.unobserve(img);
    }
  });
}, {
  rootMargin: '200px 0px', // 提前 200px 加载,体验更好
});

// 绑定所有懒加载图片
document.querySelectorAll('img.lazy').forEach(img => {
  lazyImageObserver.observe(img);
});

场景二:无限滚动 (Infinite Scroll)

在列表底部放一个看不见的“哨兵”元素。

const loadMoreObserver = new IntersectionObserver((entries) => {
  const sentinel = entries[0]; // 通常只有一个哨兵
  if (sentinel.isIntersecting) {
    console.log('触底了,请求下一页数据...');
    // fetchNextPageData();
  }
});

// 观察底部的哨兵元素
loadMoreObserver.observe(document.querySelector('#sentinel'));


7. 开发避坑指南 (Gotchas)

  1. root 必须是祖先: root 元素必须是目标元素 (target) 的祖先节点,否则观察无效。
  2. 元素必须有宽高: 如果一个元素高度为 0 或 display: none,它通常不会被检测到交叉。
  3. rootMargin 格式: 必须写单位!写 rootMargin: '10 20' 是错的,必须是 '10px 20px'
  4. 回调不仅在进入时触发: 离开时也会触发!所以务必在回调里判断 if (entry.isIntersecting)