背景
在实际项目中,经常遇到需要滚动加载数据的场景,比如消息列表、图片列表等。为了方便后续遇到相似场景可以很快的找到实现的逻辑代码,本文整理了日常工作中我常用的两种方法。
技术背景
本文示例基于 React + Tailwind 技术栈。
目录
- 原生 onScroll 事件实现滚动加载
- 原生 IntersectionObserver API 实现滚动加载
原生 onScroll 事件实现滚动加载
描述
onScroll 事件既可以监听 window 也可以监听某个元素。用它实现滚动加载的核心是基于滚动元素的 scrollTop
(元素滚动的距离) 和 scrollHeight
(滚动元素的高度) 以及 clientHeight
(元素可视区域的高度) 配合起来的差值计算作为判断。
- 判断滚动元素是否处于顶部位置,判断其
scollTop === 0
即可,也可以根据实际情况判断稍微大一点的差值,比如scollTop <= 你定义的值
- 判断滚动元素是否处于底部位置,判断其属性
scrollHeight - scrollTop - clientHeight === 0
,也可以根据实际情况判断稍微大一点的差值,比如scrollHeight - scrollTop - clientHeight <= 你定义的值
示例代码
javascript
import React, { useRef, useEffect, useState } from 'react';
import _ from 'loadsh'
const ScrollMore = () => {
const [data, setData] = useState({ count: 0 })
const [msg, setMsg] = useState('暂无操作')
const scrollHandle = _.throttle((e) => {
const current = e.target
if (current.scrollHeight - current.scrollTop - current.clientHeight < 1) {
// 滚动到底部,做你想做的其他事
setData({ count: data.count + 1 })
setMsg('到底了,数据 + 1')
} else if (current.scrollTop < 1) {
// 滚动到顶部,做你想做的其他事
setData({ count: data.count - 1 })
setMsg('到顶了,数据 - 1')
} else {
setMsg('暂无操作')
}
}, 100)
return (
<div className='mt-[20px] ml-[100px]'>
<h1 className='text-[18px] font-[700]'>onScroll 事件实现滚动加载</h1>
<div>数据变化:<span className='ml-[10px]'>{data.count}</span></div>
<div className='mt-[20px]'>{msg}</div>
<div className='mt-[20px] w-[200px] h-[680px] overflow-auto bg-[#ccc]'
onScroll={scrollHandle}>
<div className='h-[1200px]' ></div>
</div>
</div >
);
};
export default ScrollMore;
tips:实际项目中可以使用类似 loadsh 的工具库,对 onScroll 事件处理函数做一层节流处理,降低其触发的频率。
效果演示
原生 IntersectionObserver API 实现滚动加载
描述
IntersectionObserver API 是一种用于监听页面元素可见性变化的浏览器 API,它提供了一种通过观察目标元素与其祖先或视窗(viewport)之间的交叉区域来实现延迟加载、懒加载,或者自动执行其他操作的方式。详情可以参考 MDN说明
用它实现滚动加载的原理就是在滚动容器中,放置一个不影响页面渲染的元素。实际项目中可以将其透明度设置为 0,大小设置为 1px * 1px,然后监听其是否处于视窗可视区域,如果是则认为滚动元素处于底部或顶部位置。
示例代码
javascript
import React, { useRef, useEffect, useState } from 'react';
const Scroll = () => {
const topTriggerRef = useRef(null); // 用于判断是否到顶部的占位元素
const bottomTriggerRef = useRef(null); // 用于判断是否到底部的占位元素
const data = useRef({ count: 0 })
const [msg, setMsg] = useState('暂无操作')
// 在这里声明observer变量
useEffect(() => {
// 创建 IntersectionObserver 实例
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (entry.target === bottomTriggerRef.current) {
// 滚动到底部,做你想做的其他事
data.current.count += 1
setMsg('到底了,数据 + 1')
} else if (entry.target === topTriggerRef.current) {
// 滚动到底部,做你想做的其他事
data.current.count -= 1
setMsg('到顶了,数据 - 1')
}
} else {
setMsg('暂无操作')
}
});
}, {
root: null, // 视口作为参照物
rootMargin: '0px',
threshold: 1.0, // 触发时机,1.0 表示目标元素完全在视口内
});
// 如果 bottomTriggerRef 当前指向的元素存在,则开始观察
if (bottomTriggerRef.current) {
observer.observe(bottomTriggerRef.current);
}
if (topTriggerRef.current) {
observer.observe(topTriggerRef.current);
}
// 需要返回的清理函数
return () => observer.disconnect();
}, []); // 空依赖数组确保仅在组件挂载时执行
return (
<div className='mt-[20px] ml-[100px]'>
<h1 className='text-[18px] font-[700]'>IntersectionObserver API实现滚动加载</h1>
<div>数据变化:<span className='ml-[10px]'>{data.current.count}</span></div>
<div className='mt-[20px]'>{msg}</div>
<div className='mt-[20px] w-[200px] h-[680px] overflow-auto bg-[#ccc]'>
<div ref={topTriggerRef} className='text-center '>用于检测顶部的元素</div>
<div className='h-[1200px]' ></div>
<div ref={bottomTriggerRef} className='text-center '>用于底部检测的元素</div>
</div>
</div >
);
};
export default Scroll;
效果演示
注意事项
如果你仔细观察上面的代码,会发现跟 onScroll 的案例有细微的差别,就是对数据 data 我没有使用 useState,而使用的是 useRef,如果你感兴趣的话,可以去尝试一下,看看会发生什么。
这里我直接说结论,在 IntersectionObserver 的回调方法中,使用 useState,无法按预期的达到我们改变 state 的效果。
本文还是以案例为主,因此不过多的分析原理,这里贴出来一个 Stackoverflow 中有人遇到的相似问题,里面有相关原理回答,感兴趣的同学可以深入研究一下。问题地址:usestate-and-intersectionobserver-test-on-react
同理,我认为这不是个例,在我之前的文章 实战案例:ChatGPT 打字机效果的三种实现方式 里,在函数组件中使用 @microsoft/fetch-event-source
库时也遇到了类似的问题。
因此建议大家在实际项目中遇到无法按预期改变 state 时,可以尝试以下两种解决方案:
- 使用 useRef 来定义你的变量。
- 将函数组件改为类组件,通过 this.setState(obj,callback) 的回调函数 callback 来实现 state 改变之后你的预期逻辑。
杂谈
- 上述案例只是说明了最基础的使用方式,如果你想将其变为可以复用的 hook 或工具类,可以考虑让 AI 助手帮你完成,然后做简单调试即可。
- 上述的两个方案各有利弊,个人认为:
- onScroll 方案相对稳定,计算距离比较准确一些,但是需要在使用时做好类似节流的优化,避免多次触发 onScroll 的事件处理函数,从而引发性能问题。
- IntersectionObserver 对性能这块相对比较友好,但是它过于灵敏,因此如何控制好元素显隐逻辑处理会是一个比较大的考验。
- React 中关于 setState 更新的问题,在项目开发中很难遇到,但当我们遇到了,可以尝试改变策略,使用最小案例原则,尽可能复现问题,区分是 React 框架的问题还是我们逻辑代码的问题。