认识
- 浏览器自带的适用于 「监听元素与视窗交叉状态」 的观察器:「
IntersectionObserver(交叉观察器)
」 -
IntersectionObserver 是一种 JavaScript API,它提供了一种异步监测元素与其祖先容器或视口之间交叉状态的方法。简单来说,它可以告诉我们一个元素是否进入了视口或者与其祖先容器发生了交叉。
-
通过 IntersectionObserver,我们可以轻松地监听目标元素的可见性变化,进而根据这些变化来实现各种交互效果,比如懒加载图片、实现无限滚动等功能。相较于传统的事件监听方式,IntersectionObserver 更高效、灵活,可以提供更好的用户体验和性能优化。
-
当我们创建一个 IntersectionObserver 实例时,可以指定一个回调函数,该函数在目标元素进入或离开视口时被触发。回调函数提供了一个入参IntersectionObserverEntry,其中包含了与目标元素相关的信息,例如交叉比例、目标元素的位置和大小等。
-
IntersectionObserver 还支持设定阈值,即交叉比例的百分比,用于触发回调函数。默认情况下,当目标元素至少有 0% 进入视口时,回调函数会被触发。我们可以通过设置不同的阈值来满足不同的需求
创建
IntersectionObserver API
提供了一种创建IntersectionObserver
对象的方法,对象用于监测目标元素与视窗(viewport)的交叉状态,并在交叉状态变化时执行回调函数,回调函数可以接收到元素与视窗交叉的具体数据。
- 一个
IntersectionObserver
对象可以监听多个目标元素,并通过队列维护回调的执行顺序。 IntersectionObserver
特别适用于:滚动动画、懒加载、虚拟列表等场景- 回调异步执行,不阻塞主线程。且监听不随着目标元素的滚动而触发,性能消耗极低
API
构造函数
IntersectionObserver
构造函数 接收两个参数:
-
「callback」:当元素可见比例达到指定阈值后触发的回调函数
-
「options」:配置对象(可选,不传时会使用默认配置)
IntersectionObserver
构造函数 返回观察器实例,实例携带四个方法:
-
「observe」:开始监听目标元素
-
「unobserve」:停止监听目标元素
-
「disconnect」:关闭观察器
-
「takeRecords」:返回所有观察目标的
IntersectionObserverEntry
对象数组
// 调用构造函数 生成IntersectionObserver观察器
const myObserver = new IntersectionObserver(callback, options);
// 开始监听 指定元素
myObserver.observe(element);
// 停止对目标的监听
myObserver.unobserve(element);
// 关闭观察器
myObserver.disconnect();
构造参数
-
callback
回调函数,当交叉状态发生变化时(可见比例超过或者低于指定阈值)会进行调用,同时传入两个参数:- 「entries」:
IntersectionObserverEntry
数组,每项都描述了目标元素与 root 的交叉状态 - 「observer」:被调用的
IntersectionObserver
实例
- 「entries」:
-
option
配置参数,通过修改配置参数,可以改变进行监听的视窗,可以缩小或扩大交叉的判定范围,或者调整触发回调的阈值(交叉比例)。
属性 | 说明 |
---|---|
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 的时间原点到交叉被触发的时间的时间戳 |
应用
1.懒加载
使用了 IntersectionObserver API 来监听图片元素的可见性,当图片进入视口时,将图片的 data-src 属性赋值给 src,实现图片的懒加载效果
核心是延迟加载不可视区域内的资源,在元素标签中存储srcdata-src="xxx"
,在元素进入视窗时进行加载。
注意设置容器的预设高度,避免页面初始化时元素进入视窗
<div class="skin_img">
<img
class="lazyload"
data-src="//game.gtimg.cn/images/lol/act/img/skinloading/412017.jpg"
alt="灵魂莲华 锤石"
/>
</div>
.skin_img {
margin-bottom: 20px; /* 底部间距 */
width: auto; /* 宽度自适应 */
height: 500px; /* 固定高度 */
overflow: hidden; /* 溢出隐藏 */
position: relative; /* 相对定位 */
}
// 获取所有的图片节点
const imgList = [...document.querySelectorAll('img')]
// 创建一个 IntersectionObserver 实例
const observer = new IntersectionObserver((entries) => {
entries.forEach(item => {
// isIntersecting 是一个 Boolean 值,判断目标元素当前是否可见
if (item.isIntersecting) {
console.log(item.target.dataset.src) // 输出图片的 data-src
item.target.src = item.target.dataset.src // 加载图片
// 图片加载后即停止监听该元素
observer.unobserve(item.target)
}
})
}, {
root: document.querySelector('.root') // 指定根元素为 .root
})
// observe 遍历监听所有 img 节点
imgList.forEach(img => observer.observe(img))
2.滚动动画
用 IntersectionObserver 监听元素的可见性状态,根据元素是否进入视口来添加或移除类名,实现动画效果。CSS 代码定义了元素进入动画效果和关键帧动画,在元素进入视窗时添加动画样式,让内容出现的更加平滑
// 获取所有类名为 .observer-item 的元素
const elements = document.querySelectorAll('.observer-item')
// 创建一个 IntersectionObserver 实例,并传入回调函数 callback
const observer = new IntersectionObserver(callback);
// 遍历所有元素,为每个元素添加类名 opaque,并开始观察元素
elements.forEach(ele => {
ele.classList.add('opaque') // 添加类名 opaque
observer.observe(ele); // 开始观察元素
})
// 回调函数,处理 IntersectionObserver 的 entries
function callback(entries, instance) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const element = entry.target; // 获取目标元素
element.classList.remove("opaque"); // 移除类名 opaque
element.classList.add("come-in"); // 添加类名 come-in
instance.unobserve(element); // 停止观察该元素
}
})
}
.come-in {
opacity: 1; /* 不透明度为1 */
transform: translateY(150px); /* Y轴平移150px */
animation: come-in 1s ease forwards; /* 动画效果应用于 come-in 类,持续1秒,缓动效果 */
}
.come-in:nth-child(odd) {
animation-duration: 1s; /* 奇数序号元素动画持续时间为1秒 */
}
@keyframes come-in {
100% {
transform: translateY(0); /* 最终位置为Y轴向上移动0 */
}
}
3.无限滚动
React Hooks 代码,这段代码实现了一个无限滚动加载更多数据的功能,通过 IntersectionObserver 监测最后一个元素是否进入视口来触发加载更多数据的操作,添加底部占位元素lastContentRef
,在元素和视窗交叉回调时添加loading
并加载新数据
// 使用 React 的 useState 钩子定义 list 和 setList,初始值为包含 10 个 null 的数组
const [list, setList] = useState(new Array(10).fill(null));
// 使用 React 的 useState 钩子定义 loading 状态,初始值为 false
const [loading, setLoading] = useState(false);
// 使用 useRef 创建一个引用对象,并初始化为 null,用于存储最后一个元素的引用
const lastContentRef = useRef(null);
// 定义 loadMore 回调函数,使用 useCallback 包裹,确保仅在依赖项改变时重新创建
const loadMore = useCallback(async () => {
if (timer) return; // 如果 timer 存在,表示正在加载中,直接返回
setLoading(true); // 设置 loading 为 true,表示正在加载
await new Promise((resolve) => timer = setTimeout(() => resolve(timer = null), 1500); // 等待 1.5 秒
setList(prev => [...prev, ...new Array(10).fill(null)]); // 将新的 10 个 null 元素添加到列表中
setLoading(false); // 加载完成,设置 loading 为 false
}, [loading]); // 依赖 loading 状态
// 使用 useEffect 钩子监听组件挂载和更新
useEffect(() => {
// 创建一个 IntersectionObserver 实例,观察最后一个元素是否进入视口
const io = new IntersectionObserver((entries) => {
if (entries[0]?.isIntersecting && !loading) { // 如果最后一个元素进入视口且未处于加载状态
loadMore(); // 执行加载更多数据的函数
}
});
// 如果最后一个元素的引用存在,则开始观察该元素
lastContentRef?.current && io.observe(lastContentRef?.current);
}, []) // 空数组作为依赖,确保只在组件挂载时执行一次
4.虚拟列表
options
参数中的rootMargin
特别符合虚拟列表中缓存区的设计,我们再根据元素的可见性 element.visible ? content : (clientHeight || estimateHeight)
<template v-for="(item, idx) in listData" :key="item.id">
<div class="content-item" :data-index="idx">
<template v-if="item.visible">
<!-- 模仿元素内容渲染 -->
{{ item.value }}
</template>
</div>
</template>
_entries.forEach((row) => {
const index = row.target.dataset.index; // 获取元素在列表中的索引
// 判断是否在可视区域
if (!row.isIntersecting) { // 如果不在可视区域
// 离开可视区时设置实际高度进行占位 并使数据无法渲染
if (!isInitial) { // 如果不是初始渲染
row.target.style.height = `${row.target.clientHeight}px`; // 设置元素高度为实际高度,进行占位
listData.value[index].visible = false; // 将列表数据中对应项的 visible 属性设为 false,使数据无法渲染
}
} else { // 如果在可视区域
// 元素进入可视区,使数据可以渲染
row.target.style.height = ''; // 清除设置的高度,使元素恢复原始状态
listData.value[index].visible = true; // 将列表数据中对应项的 visible 属性设为 true,使数据可以渲染
}
});
这些 DOM 是用于 「占位撑起高度」 和 「供观察器监听」,在callback
时渲染成 实际内容/占位元素。
虚拟列表的核心是 「只渲染可视区内的内容」,而我们在窗口外的元素都是空div
,性能开销小到忽略不计(在页面上建10w个空div都不会卡顿)。
当然这里只是简单实现,还有很多优化方向;
-
选取部分内容监听,避免全量监听浪费资源
-
合并视窗外的元素,避免空div的性能消耗和渲染成本
-
缓存渲染完成的DOM,避免重复渲染
callback
函数会在页面加载时和每次元素交叉视窗时被调用。
callback
函数接收两个参数:一个是 IntersectionObserverEntry 对象的数组,一个是调用该函数的 IntersectionObserver 对象。IntersectionObserverEntry 对象包含了元素的交叉信息,如交叉比例(intersection ratio)和交叉区域的大小。
IntersectionObserver 随着页面滚动或元素变化来检测元素是否进入、退出视口。你需要考虑触发频率和处理操作的性能影响。避免在回调函数中执行过多的计算或复杂操作,以免降低页面的性能
兼容性
除了IE以外多数浏览器已经很好的支持了该功能
带来的的好处
-
更好的性能:传统的监听滚动事件方式可能会导致频繁的计算,影响页面性能。而 IntersectionObserver 是浏览器原生提供的 API,它使用异步执行,可以更高效地监听元素是否进入视口,减少了不必要的计算和性能开销。
-
减少代码复杂性:IntersectionObserver 可以简化代码逻辑。使用传统的方式监听滚动事件需要手动计算元素的位置、判断元素是否进入视口,以及处理滚动事件的节流等。而通过 IntersectionObserver,只需定义回调函数,在元素进入或离开视口时触发相应操作,大大简化了代码
-
支持懒加载和无限滚动:IntersectionObserver 可以实现图片懒加载和无限滚动等常见效果。当元素进入视口时,可以延迟加载图片或触发数据请求,避免不必要的资源加载,提升页面加载速度和性能。
-
更精确的可见性控制:IntersectionObserver 提供了更精确的可见性控制。通过设置合适的阈值(threshold),可以灵活地控制元素与视口的交叉区域达到多少时触发回调。这使得开发者可以根据需求来定义元素何时被认为是进入或离开视口,从而触发相应的操作。
总结
通过IntersectionObserver
我们能够轻松获取获取元素的交叉状态,除了前文中的应用,还有诸如埋点监控、视差滚动、自动播放等多种场景都可以使用IntersectionObserver
,感兴趣可以尝试。
IntersectionObserver
性能表现良好,用法简洁,能够准确把控交叉的每一个阶段。它为前端带来了更好的便利性和用户体验,非常值得尝试