IntersectionObserver 详细介绍(实现加载下一页效果)
IntersectionObserver 详细介绍
IntersectionObserver API 提供了一种异步观察目标元素与祖先元素或顶级文档视口交叉状态的方法,非常适合实现无限滚动或分页加载功能。
基本概念
IntersectionObserver 的主要用途是:
- 懒加载图片或其他内容
- 实现无限滚动网页
- 计算广告的可见度
- 根据用户是否看到相应区域来执行任务或动画
创建观察者
const observer = new IntersectionObserver(callback, options);
参数说明
-
callback:当目标元素可见性变化时调用的函数,接收两个参数:
entries
:IntersectionObserverEntry 对象数组observer
:观察者实例本身
-
options(可选):配置对象,包含以下属性:
root
:用作视口的元素,必须是目标元素的祖先,默认为浏览器视口rootMargin
:类似于 CSS 的 margin,用于扩大或缩小 root 的边界threshold
:阈值,可以是单个数字或数组,表示目标元素可见比例的百分比
IntersectionObserverEntry 对象
回调函数接收的每个 entry 对象包含以下属性:
属性 | 描述 |
---|---|
boundingClientRect | 目标元素的边界矩形 |
intersectionRatio | 目标元素的可见比例 (0.0-1.0) |
intersectionRect | 目标元素与根元素的交叉区域 |
isIntersecting | 目标元素是否与根元素相交 |
rootBounds | 根元素的边界矩形 |
target | 被观察的目标元素 |
time | 时间戳 |
常用方法
-
observe(target):开始观察指定的目标元素
observer.observe(document.getElementById('target'));
-
unobserve(target):停止观察指定的目标元素
observer.unobserve(document.getElementById('target'));
-
disconnect():停止观察所有目标元素
observer.disconnect();
-
takeRecords():返回所有观察目标的 IntersectionObserverEntry 对象数组
const entries = observer.takeRecords();
配置选项详解
root
指定作为视口检查目标可见性的元素,必须是目标元素的祖先。如果为 null
或未指定,则使用顶级文档的视口。
{root: document.querySelector('#scrollArea')
}
rootMargin
定义根元素的边距,类似于 CSS 的 margin 属性,可以用于提前或延迟触发回调。
{rootMargin: '10px 20px 30px 40px' // 上右下左
}
正值表示向外扩展,负值表示向内收缩。
threshold
指定观察器的触发阈值,可以是单个数字或数组。
{threshold: 0.5 // 当50%可见时触发
}{threshold: [0, 0.25, 0.5, 0.75, 1] // 多个触发点
}
实际应用示例
1. 图片懒加载
<img data-src="image.jpg" class="lazy-load" /><script>
const lazyImages = document.querySelectorAll('.lazy-load');const imageObserver = new IntersectionObserver((entries, observer) => {entries.forEach(entry => {if (entry.isIntersecting) {const img = entry.target;img.src = img.dataset.src;img.classList.remove('lazy-load');observer.unobserve(img);}});
});lazyImages.forEach(img => imageObserver.observe(img));
</script>
2. 无限滚动
<!DOCTYPE html>
<html>
<head><style>.item {height: 100px;margin: 10px;background: #f0f0f0;display: flex;align-items: center;justify-content: center;font-size: 24px;}.observer-target {height: 1px;}.loading-indicator {text-align: center;padding: 20px;font-size: 18px;}</style>
</head>
<body><div class="content-container"><div class="item">内容项1</div><div class="item">内容项2</div><div class="item">内容项3</div><div class="item">内容项4</div><div class="item">内容项5</div></div><div class="observer-target"></div><div class="loading-indicator" style="display: none;">加载中...</div><script>document.addEventListener('DOMContentLoaded', () => {const observerTarget = document.querySelector('.observer-target');const loadingIndicator = document.querySelector('.loading-indicator');let currentPage = 1;let isLoading = false;const totalPages = 5; // 假设总共有5页数据const observer = new IntersectionObserver((entries) => {if (entries[0].isIntersecting && !isLoading && currentPage < totalPages) {loadNextPage();}},{root: null,rootMargin: '100px', // 提前100px触发加载threshold: 0.01});observer.observe(observerTarget);async function loadNextPage() {isLoading = true;loadingIndicator.style.display = 'block';try {currentPage++;// 模拟API请求延迟await new Promise(resolve => setTimeout(resolve, 800));// 模拟获取的数据const newItems = Array.from({length: 5}, (_, i) => `内容项${(currentPage - 1) * 5 + i + 1}`);// 使用文档片段批量插入const container = document.querySelector('.content-container');const fragment = document.createDocumentFragment();newItems.forEach(itemText => {const item = document.createElement('div');item.className = 'item';item.textContent = itemText;fragment.appendChild(item);});container.appendChild(fragment);// 如果是最后一页,取消观察if (currentPage >= totalPages) {observer.unobserve(observerTarget);loadingIndicator.textContent = '已加载全部内容';}} catch (error) {console.error('加载失败:', error);currentPage--;loadingIndicator.textContent = '加载失败,请重试';setTimeout(() => {loadingIndicator.style.display = 'none';}, 2000);} finally {isLoading = false;if (currentPage < totalPages) {loadingIndicator.style.display = 'none';}}}});</script>
</body>
</html>
3. 动画触发
const animateOnScroll = new IntersectionObserver((entries) => {entries.forEach(entry => {if (entry.isIntersecting) {entry.target.classList.add('animate');animateOnScroll.unobserve(entry.target);}});
}, {threshold: 0.2});document.querySelectorAll('.animate-me').forEach(el => {animateOnScroll.observe(el);
});
性能优势
- 异步执行:不会阻塞主线程
- 高效计算:浏览器优化了交叉检测算法
- 批量处理:可以同时观察多个元素,回调中会包含所有变化
兼容性与 polyfill
IntersectionObserver 在现代浏览器中得到广泛支持,但对于旧版浏览器,可以使用官方 polyfill:
<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
或者通过 npm 安装:
npm install intersection-observer
然后在项目中引入:
import 'intersection-observer';
注意事项
- 回调执行时机:回调通常在事件循环中执行,可能不是立即的
- 大量元素观察:虽然性能较好,但观察数千个元素仍可能有性能问题
- 隐藏元素:对
display: none
的元素无效 - iframe 内容:不能直接观察 iframe 内的元素
IntersectionObserver 提供了一种高效、性能友好的方式来检测元素可见性,非常适合现代网页开发中的懒加载、无限滚动和动画触发等场景。