IntersectionObserver API
1.基本语法
const observer = new IntersectionObserver(callback, options)
2.参数说明
参数1:callback - 回调函数
(entries) => {entries.forEach((entry) => {// entry 包含被观察元素的信息})
}
entries 是一个数组,包含所有状态变化的元素信息。
每个 entry 对象包含:
entry = {target: Element, // 被观察的 DOM 元素isIntersecting: boolean, // 是否与根元素相交(是否可见)intersectionRatio: number, // 相交比例(0-1)boundingClientRect: DOMRect, // 元素的位置信息rootBounds: DOMRect, // 根元素的位置信息intersectionRect: DOMRect, // 相交区域的位置信息time: number // 时间戳
}
参数2:options - 配置对象
{root: null, // 根元素(视口或指定容器)rootMargin: '0px', // 根元素的边距threshold: [0, 1] // 触发阈值
}
- 指定一个 容器元素 作为 “观察基准”,目标元素是否 “进入”,取决于是否和这个容器重叠。
默认值 null:参照区域是 浏览器视口(整个页面的可见区域);- 必须满足:
root 必须是目标元素的 祖先元素(比如目标元素在某个滚动容器里,才能把这个容器设为 root)。 -
// 例子:监听元素是否进入「自定义滚动容器」(而非整个页面)const scrollContainer = document.getElementById('侧边栏滚动容器');const options = {root: scrollContainer, // 参照区域 = 侧边栏容器,不是整个页面rootMargin: '0px',threshold: 0};
- rootMargin: ‘0px’ → 扩展 / 缩小 “参照区域”(提前 / 延后触发)
- 作用:给 root(参照区域)加 “虚拟边距”,相当于 放大或缩小判断的 “触发范围”,让回调提前或延后触发。
- 格式和 CSS margin 完全一致:上 右 下 左(比如 100px 0 50px 0);支持 px 或 %,不能用 auto;
正值 = 扩大参照区域(提前触发),负值 = 缩小参照区域(延后触发)。
- threshold: 0 → 定义 “交叉比例”(触发的临界值)
- 作用:
指定目标元素与 root(参照区域)的 重叠比例 达到多少时,触发回调。 - 取值范围:0 ~ 1(0 = 无重叠,1 = 完全重叠);可以是单个数字(比如 0.5),也可以是数组(比如 [0, 0.5, 1] → 重叠 0%、50%、100% 时各触发一次);默认值 0:目标元素 刚碰到 参照区域(重叠比例 > 0)就触发
- 场景 1(滚动动画):希望元素 显示一半以上 时再触发动画,避免刚露个边就动:
const options = {root: null,rootMargin: '0px',threshold: 0.5 // 重叠比例≥50%才触发 };- 场景 3(多阶段触发):需要知道元素 “进入、过半、完全进入” 三个状态:
const options = {threshold: [0, 0.5, 1] // 三个阈值,触发三次回调}; - 作用:
→ 回调中可通过 entry.intersectionRatio 判断当前是哪个阶段
3.方法
observe(element) - 开始观察
observer.observe(element) // 开始观察一个元素
unobserve(element) - 停止观察
observer.unobserve(element) // 停止观察一个元素
disconnect() - 断开所有观察
observer.disconnect() // 停止观察所有元素,并清理观察器
4.使用示例
4.1.基础用法
// 创建观察器
const observer = new IntersectionObserver((entries) => {entries.forEach((entry) => {if (entry.isIntersecting) {console.log('元素进入视口:', entry.target)} else {console.log('元素离开视口:', entry.target)}})
}, {root: null,threshold: 0.5
})
4.2.懒加载图片
const imageObserver = new IntersectionObserver((entries) => {entries.forEach((entry) => {if (entry.isIntersecting) {const img = entry.targetimg.src = img.dataset.src // 加载真实图片imageObserver.unobserve(img) // 加载后停止观察}})
})document.querySelectorAll('img[data-src]').forEach(img => {imageObserver.observe(img)
})
4.3.无限滚动
const loadMoreObserver = new IntersectionObserver((entries) => {entries.forEach((entry) => {if (entry.isIntersecting) {loadMoreContent() // 加载更多内容}})
}, {root: null,rootMargin: '100px' // 提前 100px 触发
})const sentinel = document.querySelector('.load-more-sentinel')
loadMoreObserver.observe(sentinel)
4.4.锚点动态
// 观察多个锚点元素
observer = new IntersectionObserver((entries) => {entries.forEach(entry => {// 更新每个锚点的可见状态const name = entry.target.getAttribute('act-anchor-name')observedMap.set(name, { isIntersecting: entry.isIntersecting // 是否可见})})// 找到最接近视口顶部的可见锚点// 通知所有 Portal 更新激活状态
})
5.浏览器兼容性
现代浏览器:Chrome 51+、Firefox 55+、Safari 12.1+、Edge 15+
如需支持旧浏览器,可使用 polyfill
6.对比
6.1传统懒加载
“传统懒加载”,核心是通过 监听 scroll 事件 + 计算元素位置 实现;而 IntersectionObserver 是浏览器原生提供的 “交叉状态监听 API”。两者的核心差异集中在 性能、代码复杂度、功能灵活性 上
- 传统懒加载(scroll + resize + getBoundingClientRect)
- 原理:通过监听页面滚动(scroll)、窗口大小变化(resize)等事件,在事件回调中 手动计算目标元素的位置,判断是否进入视口。
- 核心步骤:
- 给 window 绑定 scroll、resize 事件(高频触发);
- 事件回调中,用 element.getBoundingClientRect() 获取元素的坐标(top/left 等);
- 对比元素坐标与视口大小(window.innerHeight/window.innerWidth),判断是否进入视口;
- 若进入视口,加载真实资源(如替换图片 src)。
传统懒加载代码示例(简化版):
<img class="lazy" src="placeholder.png" data-src="real.jpg" alt="示例">
<script>
// 1. 监听 scroll/resize 事件(需防抖优化,否则性能极差)
function debounce(fn, delay = 100) {let timer;return () => clearTimeout(timer) || (timer = setTimeout(fn, delay));
}// 2. 手动计算元素是否进入视口
function checkLazyLoad() {document.querySelectorAll('.lazy').forEach(img => {const rect = img.getBoundingClientRect();// 条件:元素顶部 ≤ 视口高度(进入视口)const isInView = rect.top <= window.innerHeight && rect.bottom >= 0;if (isInView) {img.src = img.dataset.src; // 加载真实图片img.classList.remove('lazy'); // 避免重复检查}});
}// 3. 绑定事件(防抖优化后)
window.addEventListener('scroll', debounce(checkLazyLoad));
window.addEventListener('resize', debounce(checkLazyLoad));
// 初始化时检查一次
checkLazyLoad();
</script>
- IntersectionObserver 懒加载
- 原理:浏览器原生异步监听目标元素与 “根元素”(默认视口)的 交叉状态,无需手动监听 scroll/resize,也不用手动计算位置 —— 浏览器直接告诉我们 “元素是否进入视口”。
- 核心步骤:
- 创建 IntersectionObserver 实例,配置监听规则(根元素、边距、阈值);
- 用 observe() 监听目标元素;
- 元素交叉状态变化时,浏览器触发回调,在回调中加载真实资源。
<img class="lazy" src="placeholder.png" data-src="real.jpg" alt="示例">
<script>
// 1. 创建观察器,配置规则(提前200px加载)
const observer = new IntersectionObserver((entries) => {entries.forEach(entry => {if (entry.isIntersecting) { // 浏览器直接判断:元素进入视口const img = entry.target;img.src = img.dataset.src; // 加载真实图片observer.unobserve(img); // 停止监听,避免重复触发}});
}, { rootMargin: '200px 0' }); // 提前200px加载// 2. 监听所有懒加载图片
document.querySelectorAll('.lazy').forEach(img => observer.observe(img));
</script>

优劣原因
1. 为什么是 “异步监听,不阻塞主线程”?
-
传统 scroll 方案的问题:
- scroll 是 “同步事件”,只要用户滚动页面,事件就会 高频触发(每秒几十次),且
所有回调逻辑都在 主线程 执行: - 主线程要一边处理你的 scroll 回调(计算元素位置),一边还要处理其他 JS、渲染页面、响应用户操作;
- 一旦回调逻辑复杂(比如遍历多个元素计算位置),主线程就会被 “占满”,导致页面卡顿(比如滚动不流畅、点击没反应)。
- scroll 是 “同步事件”,只要用户滚动页面,事件就会 高频触发(每秒几十次),且
-
IntersectionObserver 的设计:“后台专人盯防”
- IntersectionObserver 是 浏览器原生的异步 API,工作流程完全不占用主线程:
- 你创建观察器时,相当于告诉浏览器:“帮我盯着这些元素,它们和根元素交叉状态变了就告诉我”;
浏览器把这个 “监听任务” 交给了 合成线程(后台线程),主线程直接 “解放”,该干嘛干嘛(执行其他 JS、渲染页面); 只有当元素真的进入 / 离开视口(交叉状态变化)时,合成线程才会通过 “异步消息” 通知主线程,触发回调。简单说:传统方案是 “你自己每秒跑几十次去检查”,IntersectionObserver 是 “雇了个后台专人,有情况才告诉你”—— 主线程自然不会被阻塞。
2. 为什么 “交叉状态计算由浏览器底层完成,不会触发重排”?
- 先搞懂:什么是 “重排”?为什么传统方案会触发?
重排(Reflow):浏览器重新计算页面元素的位置、大小,是性能开销极大的操作(比如修改 width、调用 getBoundingClientRect() 都会触发);- 传统方案的 getBoundingClientRect():
每次调用都会让浏览器 “重新计算元素的实时位置”,必然触发重排 —— 而你在 scroll 回调里每秒调用几十次,相当于每秒强制浏览器重排几十次,页面不卡才怪!
- IntersectionObserver 的计算逻辑:“复用现成数据,不额外折腾”
浏览器的合成线程在处理 “滚动” 时,本身就需要知道所有元素的位置信息(才能正确渲染滚动后的页面)——IntersectionObserver 直接 “复用” 了这些现成的位置数据,完全不需要额外计算:- 合成线程维护着一个
“图层树”(记录所有元素的位置、大小、层级),滚动时会实时更新这个树;它判断 “目标元素是否和根元素交叉”,只需要对比图层树里的两个元素坐标,不需要修改 DOM、不需要重新计算布局;
整个计算过程在合成线程内部完成,完全不涉及主线程的 “重排” 逻辑。 - 简单说:传统方案是 “每次都让浏览器重新算一遍位置(重排)”,IntersectionObserver 是 “浏览器本来就知道位置,只是顺便帮你对比一下”—— 自然不会触发重排。
