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: scroll的div内部检测滚动,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 是异步批量处理的。如果你快速滚动,或者页面初始化时,多个被观察的元素可能在同一帧内状态发生改变,它们会被打包在这个数组里。
常用属性速查表:
| 属性名 | 类型 | 解释 (必记) |
|---|---|---|
isIntersecting | Boolean | 最常用。true = 元素在视口内;false = 元素离开视口。 |
target | Element | 当前发生状态变化的那个 DOM 节点。 |
intersectionRatio | Number | 当前可见比例 (0.0 - 1.0)。 |
boundingClientRect | Rect | 目标元素的矩形信息 (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)
root必须是祖先:root元素必须是目标元素 (target) 的祖先节点,否则观察无效。- 元素必须有宽高: 如果一个元素高度为 0 或
display: none,它通常不会被检测到交叉。 rootMargin格式: 必须写单位!写rootMargin: '10 20'是错的,必须是'10px 20px'。- 回调不仅在进入时触发: 离开时也会触发!所以务必在回调里判断
if (entry.isIntersecting)。