前言
在过去为了实现 懒加载、滚动动画 等需求并不容易,我们需要获取 元素与视窗的交叉状态,这通常使用 监听滚动事件 + 计算偏移量 + 判断逻辑 的方式实现,再辅以防抖节流等优化。
现如今随着技术的发展,浏览器推出了多种 观察器,让我们有更好的方式,去便捷、高效的收集页面与元素的信息。
简介
IntersectionObserver API 提供了一种创建IntersectionObserver 对象的方法,对象用于监测目标元素与视窗(viewport)的交叉状态,并在交叉状态变化时执行回调函数,回调函数可以接收到元素与视窗交叉的具体数据。
一个
IntersectionObserver 对象可以监听多个目标元素,并通过队列维护回调的执行顺序。
IntersectionObserver 特别适用于:滚动动画、懒加载、虚拟列表等场景。
监听不随着目标元素的滚动而触发,性能消耗极低。
API
构造函数
IntersectionObserver 构造函数 接收两个参数:
- callback: 当元素可见比例达到指定阈值后触发的回调函数
- options: 配置对象(可选,不传时会使用默认配置)
IntersectionObserver 构造函数 返回观察器实例,实例携带四个方法:
- observe:开始监听目标元素
- unobserve:停止监听目标元素
- disconnect:关闭观察器
- takeRecords:返回所有观察目标的
IntersectionObserverEntry对象数组
构造参数
- callback
回调函数,当交叉状态发生变化时(可见比例超过或者低于指定阈值)会进行调用,同时传入两个参数:
- entries:
IntersectionObserverEntry数组,每项都描述了目标元素与 root 的交叉状态 - observer:被调用的
IntersectionObserver实例
注册的回调函数将会在主线程中被执行。所以该函数执行速度要尽可能的快。如果需要执行任何耗时的操作,请使用
Window.requestIdleCallback()
- options
配置参数,通过修改配置参数,可以改变进行监听的视窗,可以缩小或扩大交叉的判定范围,或者调整触发回调的阈值(交叉比例)。
| 属性 | 说明 |
|---|---|
| root | 所监听对象的具体祖先元素,默认使用顶级文档的视窗(一般为html)。 |
| rootMargin | 计算交叉时添加到根(root)边界盒bounding box的矩形偏移量, 可以有效的缩小或扩大根的判定范围从而满足计算需要。所有的偏移量均可用像素(px)或百分比(%)来表达, 默认值为"0px 0px 0px 0px"。 |
| threshold | 一个包含阈值的列表, 按升序排列, 列表中的每个阈值都是监听对象的交叉区域与边界区域的比率。当监听对象的任何阈值被越过时,都会触发callback。默认值为0。 |
- IntersectionObserverEntry
| 属性 | 说明 |
|---|---|
| boundingClientRect | 返回包含目标元素的边界信息,返回结果与element.getBoundingClientRect() 相同 |
| intersectionRatio | 返回目标元素出现在可视区的比例 |
| intersectionRect | 用来描述root和目标元素的相交区域 |
| isIntersecting | 返回一个布尔值,下列两种操作均会触发callback:1. 如果目标元素出现在root可视区,返回true。2. 如果从root可视区消失,返回false |
| rootBounds | 用来描述交叉区域观察者(intersection observer)中的根. |
| target | 目标元素:与根出现相交区域改变的元素 (Element) |
| time | 返回一个记录从 IntersectionObserver 的时间原点到交叉被触发的时间的时间戳 |
案例代码(列表序号存在混乱)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Virtual List Example</title>
<style>
#container {
height: 500px;
overflow-y: auto;
border: 1px solid #ccc;
}
.list-item {
height: 50px;
background-color: #f0f0f0;
padding: 10px;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="container" style="height: 500px; overflow-y: auto;">
<div id="list" style="position: relative; height: 50000px;">
<!-- 动态生成的列表项 -->
</div>
</div>
<script>
const container = document.getElementById('container');
const list = document.getElementById('list');
const itemHeight = 50;
const totalItems = 1000;
const visibleItemCount = 10; // 固定显示的元素数量
let items = [];
function initialize() {
for (let i = 0; i < visibleItemCount; i++) {
const item = document.createElement('div');
item.className = 'list-item';
item.style.height = `${itemHeight}px`;
list.appendChild(item);
items.push(item);
}
container.addEventListener('scroll', handleScroll);
handleScroll(); // 初始化时也更新一次
}
function handleScroll() {
const scrollTop = container.scrollTop;
const visibleTop = scrollTop;
const visibleBottom = scrollTop + container.offsetHeight;
const firstVisibleIndex = Math.floor(visibleTop / itemHeight);
const lastVisibleIndex = Math.ceil(visibleBottom / itemHeight);
for (let i = 0; i < visibleItemCount; i++) {
const item = items[i];
const index = firstVisibleIndex + i;
if (index < totalItems) {
item.textContent = `Item ${index}`;
item.style.transform = `translateY(${index * itemHeight}px)`;
} else {
item.textContent = '';
item.style.transform = `translateY(${(totalItems - 1) * itemHeight}px)`;
}
}
}
const observerOptions = {
root: container,
rootMargin: '0px',
threshold: 0.1
};
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const index = entry.target.dataset.index;
loadItem(entry.target, index);
}
});
}, observerOptions);
function createListItem(index) {
const item = document.createElement('div');
item.className = 'list-item';
item.style.height = `${itemHeight}px`;
item.dataset.index = index;
return item;
}
function loadItem(item, index) {
item.textContent = `Item ${index}`;
item.style.transform = `translateY(${index * itemHeight}px)`;
}
function initializeObserver() {
for (let i = 0; i < visibleItemCount; i++) {
const item = createListItem(i);
list.appendChild(item);
observer.observe(item);
}
}
initialize();
initializeObserver();
</script>
</body>
</html>