无限滚动加载时怎么避免重复请求?

Designer°瑞君 阅读 51

我在做商品列表的无限滚动,每次滚动到底部就触发加载下一页,但有时候用户滚太快,会连续发好几次请求,导致数据重复或者接口压力大,咋办?

我试过用一个 loading 状态锁住,但好像还是会有问题,比如:

window.addEventListener('scroll', () => {
  if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
    if (!isLoading) {
      isLoading = true;
      fetchMoreData();
    }
  }
});

但偶尔还是会触发两次,是不是判断条件不够严谨?

我来解答 赞 6 收藏
二维码
手机扫码查看
2 条解答
IT人增芳
你这个问题我之前踩过坑,说白了就是 scroll 事件触发太频繁了,一瞬间可能触发好多次,isLoading 还没变 true 呢,条件判断已经跑了好几遍。

最简单的修复是加个节流,把 scroll 事件的频率降下来:

let isLoading = false;
let throttleTimer = null;

window.addEventListener('scroll', () => {
if (throttleTimer) return;

throttleTimer = setTimeout(() => {
throttleTimer = null;
}, 300); // 300ms 内只触发一次

if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
if (!isLoading) {
isLoading = true;
fetchMoreData();
}
}
});

function fetchMoreData() {
fetch('/api/products?page=' + page)
.then(res => res.json())
.then(data => {
// 渲染数据
renderData(data);
isLoading = false; // 完成后重置
})
.catch(() => {
isLoading = false; // 错误也要重置
});
}


不过更推荐用 IntersectionObserver 来做这个, 比 scroll 事件靠谱多了:

const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !isLoading) {
isLoading = true;
fetchMoreData();
}
});
}, {
rootMargin: '100px' // 距离底部 100px 时就触发
});

observer.observe(document.querySelector('.sentinel'));


这里关键是放一个看不见的 DOM 元素在列表底部,当它进入视口时就触发加载。

还有个小坑要提醒你:网络请求本身也要做好保护,比如用请求 ID 或者 cancel token,确保先发的请求后返回不会覆盖正确的数据,不过这种情况比较少见,你先用上面两种方法应该能解决个八九不离十。
点赞 1
2026-03-11 20:05
端木艺菲
哈哈这个坑我也踩过!你那个方案思路是对的,但确实可能被快速滚动触发多次。我给你两个实用的改进方法:

第一种是加个防抖(debounce),简单粗暴有效:
let debounceTimer;
window.addEventListener('scroll', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100 && !isLoading) {
isLoading = true;
fetchMoreData();
}
}, 200); // 200ms内连续滚动只触发一次
});


第二种是更严谨的锁机制,配合请求标记:
let loadingLock = false;
let currentPage = 1;

async function fetchMoreData() {
if (loadingLock) return;
loadingLock = true;

try {
const data = await fetch(/api/data?page=${currentPage});
// 处理数据...
currentPage++;
} finally {
loadingLock = false;
}
}


个人推荐第一种,简单够用。第二种适合更复杂的场景。还有个隐藏技巧是把触发点调高点,比如改成 document.body.offsetHeight - 300 提前触发,给网络请求留缓冲时间。

对了,记得在fetch完成后才把isLoading设回false,我见过有人把这个写在fetch前面导致bug的(别问我是怎么知道的...)
点赞
2026-03-10 13:01