无限滚动与分页加载
场景
需要实现一个信息流列表,滚动到底部自动加载更多数据。
实现方案
IntersectionObserver 方案(推荐)
useInfiniteScroll Hook
import { useRef, useEffect, useCallback, useState } from 'react';
function useInfiniteScroll<T>(
fetchFn: (page: number) => Promise<{ data: T[]; hasMore: boolean }>
) {
const [items, setItems] = useState<T[]>([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const sentinelRef = useRef<HTMLDivElement>(null);
const loadMore = useCallback(async () => {
if (isLoading || !hasMore) return;
setIsLoading(true);
try {
const { data, hasMore: more } = await fetchFn(page);
setItems((prev) => [...prev, ...data]);
setHasMore(more);
setPage((p) => p + 1);
} finally {
setIsLoading(false);
}
}, [page, isLoading, hasMore, fetchFn]);
useEffect(() => {
const sentinel = sentinelRef.current;
if (!sentinel) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) loadMore();
},
{ rootMargin: '200px' } // 提前 200px 触发
);
observer.observe(sentinel);
return () => observer.disconnect();
}, [loadMore]);
return { items, isLoading, hasMore, sentinelRef };
}
// 使用
function FeedList() {
const { items, isLoading, hasMore, sentinelRef } = useInfiniteScroll(fetchPosts);
return (
<div>
{items.map((item) => <PostCard key={item.id} data={item} />)}
{isLoading && <Spinner />}
{/* 哨兵元素:进入视口时触发加载 */}
<div ref={sentinelRef} />
{!hasMore && <p>已经到底了</p>}
</div>
);
}
结合虚拟列表
当数据量超过数千条时,需要虚拟滚动避免 DOM 过多:
react-window + 无限滚动
import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
function VirtualInfiniteList({ items, hasMore, loadMore }: Props) {
return (
<InfiniteLoader
isItemLoaded={(index) => index < items.length}
itemCount={hasMore ? items.length + 1 : items.length}
loadMoreItems={loadMore}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
ref={ref}
height={600}
itemCount={items.length}
itemSize={80}
onItemsRendered={onItemsRendered}
>
{({ index, style }) => (
<div style={style}>
<PostCard data={items[index]} />
</div>
)}
</FixedSizeList>
)}
</InfiniteLoader>
);
}
常见面试问题
Q1: 无限滚动相比传统分页有什么优缺点?
答案:
| 维度 | 无限滚动 | 传统分页 |
|---|---|---|
| 用户体验 | 流畅,无中断 | 需要点击翻页 |
| SEO | 差(单页内容) | 好(每页独立 URL) |
| 性能 | 需虚拟列表优化 | 每页 DOM 固定 |
| 定位 | 难以分享特定位置 | URL 包含页码 |
| 适用 | 信息流、社交媒体 | 搜索结果、表格 |
Q2: 为什么用 IntersectionObserver 而不是 scroll 事件?
答案:
IntersectionObserver 在浏览器合成线程中执行,不阻塞主线程,性能比 scroll 事件好很多。而且不需要手动节流,API 简洁。