IntersectionObserver 是浏览器原生提供的「异步交叉观察器」,用来高效监听「目标元素与其祖先或视口是否相交」以及「相交比例变化」。相比传统的 scroll + getBoundingClientRect 方案,它把计算工作下沉到浏览器内核,不阻塞主线程、无需手动节流、精度高、代码少,任何需要「元素可见性」判断的场景都能用它。
一、核心 API(记忆 3 个步骤)
- 新建观察者
const io = new IntersectionObserver(callback, options)
- 告诉它要观察谁
io.observe(DOM元素)
可多次调用,一个实例可同时观察 N 个节点 - 用完收回
io.unobserve(元素)
或io.disconnect()
callback 会被传入两个形参:
entries
-- 本次发生变化的 IntersectionObserverEntry 数组
observer
-- 当前观察者实例,可用于 unobserve
entry 上最常用的 3 个字段
isIntersecting
布尔值 -- 是否「可见」intersectionRatio
0-1 -- 可见比例target
-- 被观察的 DOM 节点
options 3 选 1 均可省
root
默认 null(= 视口),也可指定祖先滚动容器rootMargin
扩大/缩小触发范围,写法同 CSS margin("50px 0")threshold
触发阈值,默认 0(刚碰到就触发),可写数组 [0, 0.5, 1]
二、4 个常见真实场景与可直接粘贴的示例
- 图片懒加载(最经典)
html
<img class="lazy" src="placeholder.png" data-src="real.jpg" width="400">
<img class="lazy" src="placeholder.png" data-src="real2.jpg" width="400">
<script>
const io = new IntersectionObserver((entries, ob) => {
entries.forEach(en => {
if (en.isIntersecting) { // 进入视口
const img = en.target;
img.src = img.dataset.src; // 换真实地址
ob.unobserve(img); // 立即取消监听
}
});
}, { rootMargin: '100px' }); // 提前 100px 开始加载
document.querySelectorAll('img.lazy').forEach(el => io.observe(el));
</script>
- 无限滚动(触底加载下一页)
html
<ul id="list"></ul>
<li id="sentinel">加载中...</li>
<script>
let page = 1;
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) { // 底部元素完全可见
loadMore(); // 请求接口
}
}, { threshold: 1 });
function loadMore() {
fetch(`/api/list?page=${page++}`)
.then(r => r.json())
.then(arr => {
const ul = document.getElementById('list');
arr.forEach(d => ul.insertAdjacentHTML('beforeend', `<li>${d}</li>`));
});
}
io.observe(document.getElementById('sentinel'));
</script>
- 元素出现即播放动画(一次性)
css
.fadeIn { opacity: 0; transform: translateY(20px); transition: .6s }
.fadeIn.show { opacity: 1; transform: translateY(0) }
html
<div class="fadeIn">Hello</div>
<div class="fadeIn">World</div>
<script>
const io = new IntersectionObserver((entries, ob) => {
entries.forEach(en => {
if (en.isIntersecting) {
en.target.classList.add('show');
ob.unobserve(en.target); // 动画只需一次
}
});
}, { threshold: .3 }); // 30% 可见就触发
document.querySelectorAll('.fadeIn').forEach(el => io.observe(el));
</script>
- 广告/组件「曝光埋点」
javascript
const io = new IntersectionObserver((entries, ob) => {
entries.forEach(en => {
if (en.isIntersecting && en.intersectionRatio >= 1) {
// 整卡完全可见,只发一次
gtag('event', 'ad_impression', { element: en.target.dataset.id });
ob.unobserve(en.target);
}
});
}, { threshold: 1 });
document.querySelectorAll('.ad-card').forEach(card => io.observe(card));
三、易踩小坑 & 性能提示
- 观察器实例可以复用,不要给每个元素 new 一个
- 回调里不要做重计算/同步 IO,必要时用
requestIdleCallback
延后 - 节点移除后记得
unobserve
,SPA 切换页时记得disconnect()
防止泄漏 - 旧版浏览器(IE)需加载 polyfill(github.com/w3c/IntersectionObserver)
- 若滚动容器不是 window,记得把
root
指向那个「可滚动祖先」
四、一句话总结
IntersectionObserver = 「可见性变化」专属黑科技:
代码更少、性能更好、功能刚好,懒加载、无限滚、动画、埋点全能打。今天开始,把 scroll 事件收起来吧!