這次,我終于把無限滾動寫對了
無限滾動這個(gè)功能,如果實(shí)現(xiàn)得當(dāng),簡直是絲滑流暢;但一旦出問題,體驗(yàn)立馬崩塌。
相信你也遇到過這種情況:
剛想點(diǎn)擊某個(gè)內(nèi)容,頁面突然加載新數(shù)據(jù),元素跳動,手指點(diǎn)空,一臉問號。
為了不重蹈覆轍,我決定在我的 React 項(xiàng)目中,“認(rèn)真” 實(shí)現(xiàn)一次無限滾動 —— 要性能好、體驗(yàn)穩(wěn)、不卡頓、無抖動。
圖片
核心挑戰(zhàn):讓滾動真正「無限」且「順滑」
從用戶角度看,無限滾動就是:滾到頁面底部,自動加載更多內(nèi)容。
聽起來簡單,實(shí)現(xiàn)起來卻暗藏不少坑:
- 性能問題:如果監(jiān)聽 scroll 事件不加控制,很容易導(dǎo)致頁面卡頓;
- 重復(fù)請求:過度觸發(fā)接口,容易把服務(wù)器拉爆,甚至被限流;
- UI 抖動:加載數(shù)據(jù)位置處理不當(dāng),頁面就會跳動、錯位,影響體驗(yàn)。
于是,我換了種思路,從根源解決這些問題。
核心工具:Intersection Observer API
傳統(tǒng)方式一般是監(jiān)聽 scroll 事件 + 判斷滾動位置。但這種方式有幾個(gè)缺陷:
- 滾動監(jiān)聽觸發(fā)頻繁,性能壓力大;
- 需要手動計(jì)算位置,誤差多;
- 難以精準(zhǔn)判斷「用戶是否到達(dá)底部」。
所以我選擇了 Intersection Observer。
這是一種現(xiàn)代瀏覽器提供的異步監(jiān)聽 API,能精準(zhǔn)判斷某個(gè)元素是否進(jìn)入視口 —— 完美適配無限滾動的“觸底加載”場景。
為什么選它?
- ? 高性能:異步觸發(fā),瀏覽器原生優(yōu)化,幾乎零性能負(fù)擔(dān);
- ? 高精度:只在元素真的進(jìn)入視口時(shí)才觸發(fā);
- ? 易管理:不再需要頻繁監(jiān)聽 scroll 事件或手動判斷位置。
實(shí)現(xiàn)步驟(以 React 為例)
1. 初始化組件狀態(tài)
用于管理:當(dāng)前數(shù)據(jù)列表、加載狀態(tài)、頁碼、是否還有更多數(shù)據(jù)等。
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);2. 編寫數(shù)據(jù)請求函數(shù)
根據(jù)頁碼分頁請求數(shù)據(jù),并更新狀態(tài)。
const fetchData = async () => {
if (isLoading || !hasMore) return;
setIsLoading(true);
const res = await fetch(`/api/list?page=${page}`);
const newItems = await res.json();
setItems(prev => [...prev, ...newItems]);
setHasMore(newItems.length > 0);
setPage(prev => prev + 1);
setIsLoading(false);
};3. 設(shè)置 Intersection Observer
使用 useEffect 監(jiān)聽底部 sentinel 元素是否進(jìn)入視口。
const observer = useRef();
useEffect(() => {
if (isLoading) return;
const target = document.querySelector("#sentinel");
if (!target) return;
observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasMore) {
fetchData();
}
});
observer.current.observe(target);
return () => observer.current?.disconnect();
}, [isLoading, hasMore]);4. 渲染列表與 sentinel 元素
return (
<div className="list">
{items.map(item => (
<Item key={item.id} data={item} />
))}
{isLoading && <LoadingSpinner />}
<div id="sentinel" />
</div>
);實(shí)用建議與細(xì)節(jié)處理
?? 節(jié)流 / 防抖:可以給 fetchData 添加 debounce,防止用戶滾得太快導(dǎo)致并發(fā)請求。
?? 無更多數(shù)據(jù)提示:加個(gè) hasMore 判斷,避免接口反復(fù)觸發(fā)。
?? 組件卸載時(shí)清理:別忘了在 useEffect 中 disconnect(),防止內(nèi)存泄漏。
最終效果
通過 Intersection Observer API,我的無限滾動實(shí)現(xiàn)終于達(dá)到了「絲滑不抖動、加載不卡頓、邏輯清晰」的理想狀態(tài)。
和傳統(tǒng) scroll 監(jiān)聽相比:
- 不用手動算距離;
- 性能更優(yōu);
- 用戶體驗(yàn)更流暢。
總結(jié)
無限滾動并不難,關(guān)鍵在于用對工具,處理好邊界。
? 使用 Intersection Observer 實(shí)現(xiàn)精準(zhǔn)觸發(fā);
? 結(jié)合防抖和 hasMore 判斷避免重復(fù)請求;
? 做好清理,保持組件干凈;
這套方案,在中小規(guī)模列表加載中非常穩(wěn)定。
如果你也曾經(jīng)或正在實(shí)現(xiàn)無限滾動,不妨試試這種方式。
有經(jīng)驗(yàn)、有坑、有疑問?歡迎留言,一起討論。
































