React自定义Hook之useInView可视区检测
简介
本文主要讲述如何实现元素的可视区检测,并封装一个基本的React Hook,当项目中需要实现可视区检测相关的功能,例如监听元素是否进入视口、图片懒加载、动画触发等功能时,可以使用 useInView
。本文将从原理出发,简要介绍 useInView
的实现,并从实际应用场景帮助读者理解如何使用它。
一、原理
1. Intersection Observer API
Intersection Observer 是浏览器提供的一个API,提供了一种异步检测目标元素与祖先元素或 视口(viewport) 相交情况变化的方法。
传统的可视区检测,一般采用监听滚动事件计算offsetTop、scrollTop或者频繁调用Element.getBoundingClientRect()
来获取元素的边界信息进行计算。
而 IntersectionObserver
提供了一种更高效的监听元素是否在当前视窗内的能力,下面我们一起来看下它的用法。
2. 用法
创建一个 IntersectionObserver 对象,并传入回调用函数和相应参数,该回调函数将会在目标 (target) 元素和根 (root) 元素的交集大小超过阈值 (threshold) 规定的大小时触发。
const options = {
root: null, // 为null则默认为视口viewport
rootMargin: "0px",
threshold: 1, // 阈值为1,元素完全进入才触发回调
};
const observer = new IntersectionObserver(callback, options);
上面代码中,callback
函数的参数接收两个参数entries
和observer
:
entries
:这是一个数组,每个成员都是一个被观察对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,那么entries
数组里面就会打印出两个元素,如果只观察一个元素,我们打印entries[0]
就能获取到被观察对象observer
: 这个是监视器本身
3. 应用场景:
(1)监听元素是否进入视口,上报访问次数,可用于数据埋点
let count = 0
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// 元素可见
if (entry.isIntersecting) {
count = count + 1
// 上报访问次数
fetchReport(count)
}
});
}, options);
const target = document.getElementById('reportTarget')
observer.observe(target)
(2)无限滚动
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadMore()
}
});
}, options);
const loader = document.getElementById('loader')
observer.observe(loader)
(3)图片懒加载
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
const imgs = document.querySelectorAll('.img');
imgs.forEach((img) => {
observer.observe(img);
});
4. Intersection Observer 优点
- 异步触发:
IntersectionObserver
观察器的优先级非常低,采用了requestIdleCallback()
,不会占用主线程资源,只有在浏览器空闲下来才会执行观察器。 - 减少开销:相比较传统的监听滚动事件,
IntersectionObserver
避免了频繁的触发和计算,节省了资源。 - 批量观察元素:可以一次性观察多个目标元素,以数组形式存储在callback函数的entries参数中,方便开发者进行批量操作。
- 浏览器兼容性好(低版本使用参考 IntersectionObserver polyfill)
二、基于Intersection Observer API封装useInView
1. useInView
,直接上代码
import { useEffect, useState, useRef } from 'react';
const useInView = (
options = {
root: null,
rootMargin: '0px 0px',
threshold: 1,
},
triggerOnce = false, // 是否只触发一次
) => {
const [inView, setInView] = useState(false);
const targetRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setInView(true);
if (triggerOnce) {
// 触发一次后结束监听
observer.unobserve(entry.target);
}
} else {
setInView(false);
}
});
}, options);
if (targetRef.current) {
// 开始监听
observer.observe(targetRef.current);
}
return () => {
if (targetRef.current) {
// 组件卸载时结束监听
observer.unobserve(targetRef.current);
}
};
}, [options, triggerOnce]);
return [targetRef, inView];
};
export default useInView;
2. 使用示例
import React from 'react';
import useInView from './hooks/useInView';
const App = () => {
const [targetRef, inView] = useInView();
return (
<div>
<div style={{ height: '100vh' }}></div>
<div
ref={targetRef}
style={{
width: '100px',
height: '100px',
background: inView ? 'green' : 'red',
}}
>
{inView ? 'In View' : 'Out of View'}
</div>
</div>
);
};
export default App;
3. ts版本
import { useEffect, useState, useRef } from 'react';
import type { MutableRefObject } from 'react';
type TargetRef = MutableRefObject<HTMLElement | null>;
const useInView = (
options: IntersectionObserverInit = {
root: null,
rootMargin: '0px',
threshold: 1,
},
triggerOnce: boolean = false,
): [TargetRef, boolean] => {
const [inView, setInView] = useState(false);
const targetRef: TargetRef = useRef<HTMLElement | null>(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setInView(true);
if (triggerOnce) {
observer.unobserve(entry.target);
}
} else {
setInView(false);
}
});
}, options);
if (targetRef?.current) {
observer.observe(targetRef.current);
}
return () => {
if (targetRef?.current) {
observer.unobserve(targetRef.current);
}
};
}, [options, triggerOnce]);
return [targetRef, inView];
};
export default useInView;
三、总结
useInView
基于 Intersection Observer API
进行封装,开发者可以更加轻松地实现视口检测相关的功能,如埋点数据上报、动画触发等。旨在为前端项目提供了更高效的能力,同时也能优化性能。无论你是在开发单页应用还是多页应用,都可以根据实际需求灵活运用useInView
,从而为用户创造出更流畅的浏览体验。