「滾動到底部加載更多」是無限滾動核心需求。但 `scrollTop + clientHeight >= scrollHeight` 看似簡單,實則暗藏 **CSS1Compat 模式差異、文檔/元素滾動容器混淆、邊界閾值抖動** 等大坑。本文提供跨瀏覽器、高性能、生產級的底部檢測方案。
在 **標準模式** (``) 下:
/**
* 判斷是否滾動到底部 (含閾值緩衝)
* @param {number} threshold - 提前觸發閾值(像素),默認 0 即精確底部
* @param {Element} container - 滾動容器,默認 window (文檔)
* @returns {boolean}
*/
function isScrolledToBottom(threshold = 0, container = window) {
// 1. 獲取滾動容器的關鍵尺寸
let scrollTop, scrollHeight, clientHeight;
if (container === window) { // 文檔滾動
const doc = document.documentElement; // 標準模式關鍵!
const body = document.body;
// 兼容性獲取 scrollTop (標準模式用 doc, 怪異模式/舊瀏覽器用 body)
scrollTop = window.pageYOffset || doc.scrollTop || body.scrollTop || 0;
// scrollHeight 取最大值,防止 body 有 margin 導致 doc.scrollHeight < 實際
scrollHeight = Math.max(doc.scrollHeight, body.scrollHeight, doc.offsetHeight, body.offsetHeight);
clientHeight = doc.clientHeight; // 視口高度
} else { // 元素滾動 (如 div 內部滾動)
scrollTop = container.scrollTop;
scrollHeight = container.scrollHeight;
clientHeight = container.clientHeight;
}
// 2. 核心判斷:滾動距離 + 視口高度 >= 總高度 - 閾值
return (scrollTop + clientHeight >= scrollHeight - threshold);
}
直接綁 `scroll` 事件會極其頻繁觸發 (每幀數次),導致主執行緒卡頓、重複觸發加載。必須使用 **`requestAnimationFrame` 節流** 或 **時間戳節流**。
// 方案一:requestAnimationFrame 節流 (最流暢,配合瀏覽器繪製幀)
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
ticking = true;
requestAnimationFrame(() => {
if (isScrolledToBottom(100)) { // 提前 100px 觸發加載
loadMoreData();
}
ticking = false;
});
}
}, { passive: true }); // passive: true 提升滾動性能
// 方案二:時間戳節流 (簡單,固定間隔)
let lastTime = 0;
const THROTTLE_MS = 200;
window.addEventListener('scroll', () => {
const now = Date.now();
if (now - lastTime >= THROTTLE_MS) {
lastTime = now;
if (isScrolledToBottom(50)) loadMoreData();
}
}, { passive: true });
let loading = false; // 防並發鎖
let page = 1;
const THRESHOLD_PX = 200; // 提前 200px 預加載
async function loadMoreData() {
if (loading) return;
loading = true;
showLoadingSpinner();
try {
const data = await fetch(`/api/list?page=${page++}&size=20`).then(r => r.json());
if (data.length === 0) {
showEndMessage();
window.removeEventListener('scroll', onScroll); // 移除監聽
return;
}
appendItemsToDOM(data);
} catch (e) {
console.error('加載失敗', e);
showRetryButton();
} finally {
loading = false;
hideLoadingSpinner();
}
}
function onScroll() { /* 節流邏輯調用 isScrolledToBottom(THRESHOLD_PX) */ }
window.addEventListener('scroll', onScroll, { passive: true });
| 現象 | 原因 | 修正 |
|---|---|---|
| 永遠不觸發 / 過早觸發 | 用 `document.body.scrollTop` (標準模式下恆 0) | 統一用 `document.documentElement` 或 `window.pageYOffset` |
| 觸發多次重複加載 | 無 `loading` 鎖、無節流、閾值為 0 邊界抖動 | 加 `loading` 旗標 + `requestAnimationFrame` 節流 + `threshold >= 50px` |
| 移動端地址欄隱藏導致高度變化誤判 | 視口高度 `clientHeight` 動態變化 | 用 `window.visualViewport.height` (現代移動端) 或增大閾值容忍 |
| iframe/Shadow DOM 內滾動 | 容器非 window | 傳入具體滾動容器 `element` 給 `isScrolledToBottom` |
**標準模式下認準 `document.documentElement`**,封裝兼容性 `getScrollMetrics()`,配合 **`requestAnimationFrame` 節流 + `loading` 鎖 + 預載閾值**,即可構建流暢、可靠、跨端的無限滾動體驗。避免直接複製網上簡易版 `body.scrollTop` 代碼,那是怪異模式時代的遺物。