效果展示
这次分享一个利用IntersectionObserver API
实现的无限滚动Hook,如图所示,当我们滑动到底部时就会加载数据,当数据全部加载完后,将不能继续加载了。
这样一个效果大家肯定在很多地方都有见过,如何实现呢?现在就跟着我一步步了解一下吧😉!
源码在这里👉 Ys-OoO's github link.
IntersectionObserver API
标题中提到了IntersectionObserver
,那么我们首先第一步就是要了解这个API,看看他到底提供了什么功能。
MDN中是这样介绍的:提供了一种异步检测目标元素与祖先元素或顶级文档的[视口]相交情况变化的方法。
翻译翻译:这个API实现了一个监听器,这个监听器用于 监听一个目标元素 与 他的祖先元素或浏览器视口 的交叉情况(你可以抽象地理解为两个方块的相交面积),并且这个API是异步进行检测的。为什么我要强调异步检测,具体原因大家可以去MDN上看看,这里给出结论就是使用IntersectionObserver API
在很多场景中既节省了程序员的心智消耗,又在性能上表现优异!
知道这些了,最重要的是要知道如何使用,接着看。
我们需要使用new
来创建一个IntersectionObserver对象实例:
js
const observer = new IntersectionObserbver(callback,options);
构造函数中包含两个参数:
- callback:当我们指定的目标元素与其某个祖先元素的相交比例达到某个阈值时所触发的函数。
- 单看这句话你可能还存在疑问,在这里提前试着解答:
- 指定的目标元素是在实例调用监听函数时指定的。
- 其某个祖先元素是在options中指定的
- 这个API所检测的主要依据就是两个元素相交的比例,你可以认为是
相交面积/目标元素面积
- 阈值是options中指定的
- 单看这句话你可能还存在疑问,在这里提前试着解答:
- options: 用来配置 observer 实例,他是一个对象,配置项如下:
- root: 即交叉计算时的某个祖先元素,默认值为
null
,表示顶级文档/视口 - rootMargin:计算交叉时,将其添加到祖先元素上,写法同CSS的Margin,默认值为:
"0px 0px 0px 0px"
- threshold:触发回调所设置的阈值,可以是一个数组:
[0.5,1]
,即交叉比例达到0.5和1时都会触发回调函数,默认值为0.0
,范围为[0.0,1.0]
- root: 即交叉计算时的某个祖先元素,默认值为
了解了这些还不够,还有几个需要了解的:
- callback的参数,回调函数包括两个参数:
- entries:一个
IntersectionObserverEntry
对象的数组,每个被触发的阈值,都或多或少与指定阈值有偏差。- 这里需要注意计算的交叉比例往往和设定的阈值有偏差,这很正常,不要过度依赖准确比例喔
- entries是数组 ,一个
IntersectionObserver
实例可以监听多个元素与某个父元素的交叉情况,因此是数组形式,对于数组中的每一项都是一个IntersectionObserverEntry
,点击上面的链接你可以了解更多.
- observerInstance:就是当前这个
IntersectionObserver
实例,你可以在回调中使用实例身上的方法。
- entries:一个
- 实例身上的方法,主要介绍下面三个:
- observe(targetElement):开始监听,传入被监听的目标元素
- unobserve(targetElement):停止监听,传入需要停止监听的目标元素
- disconnect(): 停止监听所有当前实例监听的目标元素。
IntersectionObserver
的介绍到此结束。
使用React + IntersectionObserver
实现无限滚动Hook
我们首先对该Hook进行分析,确定需要的参数以及返回值
分析
观察一下一开始的GIF,我们每次划到最下面的时候才需要获取更多数据,数据是否还有以及如何获取数据这些关键信息在日常开发中都是后端返回的,因此获取数据的方法 应该作为入参,而这个方法也需要进行规定,它的返回值用来告知我们新的数据以及是否还有更多数据可以加载 。此外,最关键的就是监听的目标元素以及他的父元素了,这个场景中父元素应该总是视口,因此可以不用关心,那么这个目标元素 是谁呢? 实际上就是Loading
组件,这个组件可以是我们自定义的,也可以是预设定的,因此我们需要提供加载状态、用于标识加Loading件的Ref、预设的Loading组件
综上所述:
text
useInfiniteScroll Hook:
入参:
fetchFunc():用于请求更多数据,返回请求的数据以及是否有更多数据可加载
返回值:
data: 当前请求的所有数据(合并每次请求数据后的结果集)
hasMore: 是否还有更多数据
loading:加载中的状态
loadingRef:用于标识自定义Loading组件
Loading: 预设的Loading组件
实现
分析完了,接下来就是实现了。
这里直接给出代码再进行解释:
js
function useInfiniteScroll(fetchFunc) {
const loadingRef = useRef(null);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [data, setData] = useState([]);
const loadMore = useCallback(debounce(async () => {
setLoading(true);
const { data: newData, more } = await fetchFunc?.();
setHasMore(more);
setData((prevData) => [...prevData, ...newData]);
setLoading(false);
}, 1000), []);
//注册IntersectionObserver
useEffect(() => {
const targetEle = loadingRef.current;
const observer = new IntersectionObserver((entries, curObserver) => {
if (!hasMore) {
curObserver.unobserve(targetEle);
return;
}
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMore();
}
})
}, {
root: null,
rootMargin: "0px 0px 0px 0px",
threshold: 1
});
if (targetEle) {
observer.observe(targetEle);
}
return () => {
if (targetEle) {
observer.unobserve(targetEle);
}
}
}, [loadMore,hasMore])
return {
data,
hasMore,
loading,
loadingRef,
Loading: hasMore ? <div ref={loadingRef}>Loading...</div> : <div ref={loadingRef}>无更多数据</div>,
}
}
export default useInfiniteScroll;
实际上这个Hook最关键的地方就在这个useEffect
中,首先是内部逻辑,我们创建了一个IntersectionObserver
实例,其配置为:
js
{
root: null,
rootMargin: "0px 0px 0px 0px",
threshold: 1
}
经过前面的学习这就不难理解了,其中的回调都干了什么呢?我们要判断上一次请求中得到的结果,是否还有待加载数据,也就是hasMore
,如果没有更多了,我们就可以不再执行加载了,并且取消当前的监听。
然后就是对entries进行遍历,我们通过entry.isIntersecting
就可以判断当前是否是相交状态,如果是的话就可以执行loadMore()
来加载更多数据了。
这个实例对象创建完成后,就要指定监听了,我们监听的元素即为LoadingRef
所存储的元素,当组件销毁时记得移除监听。
然后是依赖,由于在useEffect
中存在闭包问题,我们要在每次回调中获取正确的hasMore
和loadMore
,因此将其作为依赖,事实上loadMore
不会进行改变,实际上不用做为依赖,如果你像我一样将其作为了依赖,那么loadMore
请务必使用useCallBack
包裹,否则会出现因为loadMore
从无到有过程中而导致的首次执行两边的情况。
其次关注loadMore()
,这里我将其进行了防抖处理,原因大家都能想清楚就不做解释了,在内部我们设置加载状态->请求数据->更新数据和hasMore->更新加载状态
。这一套逻辑也没有需要多说的,重点在于我们更新数据的时候,新的数据是要追加到旧的数据中的,如果我们此时使用setData([...data,...newData])
,页面并不会触发重新渲染,我们为了能够拿到上一次的data需要使用setState
的回调写法setData((prevData) => [...prevData, ...newData]);
至此就全部结束了!
提问
对于上面提到的这个问题:setData([...data,...newData])
,页面并不会触发重新渲染,如果你知道或者有想法欢迎来评论区讨论一下!🙏
实际上这是一个闭包过期 的问题,与添加useEffect
的依赖时遇到的问题一样,只不过使用了不同的解决方法,之前我们也提到了,IntersectionObserver API
是一个异步的 ,如果你这里不懂得话就得先去了解闭包陷阱、闭包过期了,你会发现那些Demo常与setInterval
等这些异步函数举例。然后你可以再深入了解一下React的更新机制,这样到时候你就会有更深的理解了。
为了更好的理解我上面说的,后续我也会写一篇文章来讨论,大家记得关注喔!🙏