在React函数组件中实现基于时间间隔的分批次无限滚动,核心是通过监听页面滚动位置触发数据加载,同时用定时器控制批次请求的间隔,配合状态管理维护加载状态、数据列表和分页信息,避免重复请求和数据冲突。

核心实现思路
整个功能的实现可以分为几个核心部分:滚动位置监听、时间间隔控制、数据请求与状态更新、边界条件处理。首先需要在组件挂载时监听窗口滚动事件,判断滚动位置是否到达底部触发加载逻辑;其次通过定时器或者请求队列控制批次请求的间隔,避免短时间内发送过多请求;最后通过React的state管理加载状态、数据列表、当前页码、是否有更多数据等状态,保证数据更新的正确性。
关键状态定义
在函数组件中,我们需要定义以下几个核心状态来管理无限滚动的相关逻辑:
- list:存储已加载的所有数据列表
- page:当前请求的页码,用于分页参数
- loading:当前是否正在加载数据,避免重复请求
- hasMore:是否还有更多数据可以加载,控制是否继续触发请求
- timer:存储定时器实例,用于清理和控制请求间隔
状态定义的代码示例如下:
import { useState, useEffect, useRef } from "react";
const BatchInfiniteScroll = () => {
// 数据列表
const [list, setList] = useState([]);
// 当前页码
const [page, setPage] = useState(1);
// 加载状态
const [loading, setLoading] = useState(false);
// 是否还有更多数据
const [hasMore, setHasMore] = useState(true);
// 定时器引用
const timerRef = useRef(null);
// 容器引用,用于监听滚动
const containerRef = useRef(null);
return (
<div ref={containerRef} style={{ height: "500px", overflow: "auto" }}>
{/* 列表内容 */}
</div>
);
};
export default BatchInfiniteScroll;
滚动监听实现
我们需要判断滚动容器的滚动位置是否到达底部,这里以监听容器自身滚动为例,避免影响整个页面。滚动到底部的判断逻辑是:容器的滚动高度 - 容器的已滚动高度 - 容器的可视高度 <= 阈值,当满足条件且不在加载中、还有更多数据时,触发加载逻辑。
滚动监听的实现代码如下:
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleScroll = () => {
// 滚动阈值,距离底部50px时触发加载
const threshold = 50;
const { scrollTop, scrollHeight, clientHeight } = container;
// 判断是否滚动到底部
const isBottom = scrollHeight - scrollTop - clientHeight <= threshold;
if (isBottom && !loading && hasMore) {
// 触发加载逻辑
startLoad();
}
};
container.addEventListener("scroll", handleScroll);
return () => {
container.removeEventListener("scroll", handleScroll);
// 清理定时器
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [loading, hasMore]);
基于时间间隔的分批次加载实现
分批次加载的核心是控制两次请求之间的时间间隔,避免短时间内发送大量请求。这里使用setTimeout来控制间隔,每次触发加载时,如果当前没有定时器在等待,就设置一个定时器,间隔时间后执行数据请求。
加载逻辑的实现代码如下:
// 模拟数据请求接口,实际项目中替换为真实接口
const fetchData = (currentPage) => {
return new Promise((resolve) => {
setTimeout(() => {
// 模拟返回数据,每页10条,最多3页
const data = Array.from({ length: 10 }, (_, index) => ({
id: (currentPage - 1) * 10 + index + 1,
content: `列表项 ${(currentPage - 1) * 10 + index + 1}`
}));
const hasMoreData = currentPage < 3;
resolve({ data, hasMore: hasMoreData });
}, 500);
});
};
const startLoad = () => {
// 如果正在加载或者没有更多数据,直接返回
if (loading || !hasMore) return;
// 设置加载状态
setLoading(true);
// 如果没有定时器,设置时间间隔后执行请求
if (!timerRef.current) {
timerRef.current = setTimeout(async () => {
try {
const res = await fetchData(page);
// 拼接新数据到列表
setList(prev => [...prev, ...res.data]);
// 更新页码
setPage(prev => prev + 1);
// 更新是否有更多数据
setHasMore(res.hasMore);
} catch (error) {
console.error("数据加载失败", error);
} finally {
setLoading(false);
// 清空定时器引用
timerRef.current = null;
}
}, 1000); // 间隔1秒加载下一批
}
};
完整组件示例
将以上逻辑整合后,完整的函数组件代码如下:
import { useState, useEffect, useRef } from "react";
const BatchInfiniteScroll = () => {
const [list, setList] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const timerRef = useRef(null);
const containerRef = useRef(null);
// 模拟数据请求
const fetchData = (currentPage) => {
return new Promise((resolve) => {
setTimeout(() => {
const data = Array.from({ length: 10 }, (_, index) => ({
id: (currentPage - 1) * 10 + index + 1,
content: `列表项 ${(currentPage - 1) * 10 + index + 1}`
}));
const hasMoreData = currentPage < 3;
resolve({ data, hasMore: hasMoreData });
}, 500);
});
};
const startLoad = () => {
if (loading || !hasMore) return;
setLoading(true);
if (!timerRef.current) {
timerRef.current = setTimeout(async () => {
try {
const res = await fetchData(page);
setList(prev => [...prev, ...res.data]);
setPage(prev => prev + 1);
setHasMore(res.hasMore);
} catch (error) {
console.error("加载失败", error);
} finally {
setLoading(false);
timerRef.current = null;
}
}, 1000);
}
};
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleScroll = () => {
const threshold = 50;
const { scrollTop, scrollHeight, clientHeight } = container;
const isBottom = scrollHeight - scrollTop - clientHeight <= threshold;
if (isBottom && !loading && hasMore) {
startLoad();
}
};
container.addEventListener("scroll", handleScroll);
// 初始加载第一页数据
startLoad();
return () => {
container.removeEventListener("scroll", handleScroll);
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [loading, hasMore, page]);
return (
<div>
<h3>分批次无限滚动列表</h3>
<div
ref={containerRef}
style={{ height: "500px", overflow: "auto", border: "1px solid #eee", padding: "10px" }}
>
<ul style={{ listStyle: "none", padding: 0 }}>
{list.map(item => (
<li key={item.id} style={{ padding: "10px", borderBottom: "1px solid #f0f0f0" }}>
{item.content}
</li>
))}
</ul>
{loading && <p style={{ textAlign: "center" }}>加载中...</p>}
{!hasMore && !loading && <p style={{ textAlign: "center" }}>没有更多数据了</p>}
</div>
</div>
);
};
export default BatchInfiniteScroll;
注意事项
在实际使用中需要注意以下几点:
- 组件卸载时需要清理滚动事件监听和定时器,避免内存泄漏
- 加载状态需要严格判断,避免滚动多次触发导致重复请求
- 时间间隔可以根据实际业务需求调整,比如接口响应慢可以适当延长间隔
- 如果数据请求失败,需要增加重试逻辑,提升用户体验
- 如果列表项高度不固定,滚动到底部的判断逻辑需要调整为基于最后一个元素的位置计算