无限滚动时多次触发请求该怎么解决?
我用window.scroll监听做无限滚动,设置的防抖函数也没问题,但滚动到底部时还是会触发多次请求。比如快速滚动时甚至会连续请求三次,怎么排查这个问题?
我的逻辑是监听scroll事件,当scrollTop + clientHeight >= documentHeight时调用fetchData:
const handleScroll = debounce(() => {
const { scrollTop, clientHeight } = document.documentElement;
const documentHeight = document.body.scrollHeight;
if (scrollTop + clientHeight >= documentHeight - 200) {
fetchData(); // 这里会多次调用
}
}, 300);
尝试过加isLoading标记但没起作用,请求回来后还能继续触发。是不是防抖没生效?或者有其他边界条件没考虑到?
先说下为什么你加的
isLoading标记没起作用:你可能是在
fetchData开始时设置isLoading = true,结束时设为false,但问题在于——防抖函数本身每次都会重新计时,如果在防抖延迟时间内又触发了新的滚动事件,防抖函数会重新排队,等延迟时间一到,它会执行最后一次调用,而之前被“跳过”的调用根本没机会执行,但你可能在fetchData里又没加锁判断,导致多个fetchData实例在并发执行(尤其是网络慢的时候更容易暴露这个问题)。而且,更致命的是:防抖不能解决“请求还没回来就再次触发”的问题,它只解决“短时间内多次触发只执行最后一次”,但如果你滚动到底部后快速抖动几次,或者用户手抖多滚了几次,防抖可能只保留最后一次,但如果最后一次执行时请求还没回来,它又会发新请求。
正确做法是三层防护:
1. 用“节流 + 状态锁”代替纯防抖:防抖适合“输入框实时搜索”这种场景,但无限滚动更适合节流,因为你要确保“只要用户滚到底,就一定要发一次请求”,而不是只发最后一次。节流能保证在一定时间间隔内最多执行一次,同时配合锁机制避免并发请求。
2. 加请求状态锁,且锁必须是“滚动感知型”的:不能只靠一个全局
isLoading,因为滚动事件可能在请求发出后又触发了,导致重复判断。应该在触发请求前加锁,请求结束后立即解锁,且锁的检查和设置必须是原子操作(避免竞态)。3. 用 IntersectionObserver 代替 scroll 监听(推荐):scroll 事件太底层,性能差、边界条件多,比如滚动到末尾时
document.body.scrollHeight可能还没更新(异步渲染导致),或者滚动事件触发时 DOM 还没渲染完,导致判断不准。IntersectionObserver 是专门干这个的,它会在元素进入视口时触发,更可靠。先给你一个最小改动、能立刻用的方案(基于 scroll 监听):
这个方案里,关键点是:
- 用 节流 而不是防抖,因为无限滚动需要“只要滚到底就发一次”,不是“只发最后一次”
-
isLoading锁必须在判断滚动条件之后、发请求之前立刻设为true,避免中间有空档被其他事件抢占-
fetchData的then和catch都要解锁,不然请求失败就卡死了- 用
requestAnimationFrame包裹handleScroll能减少 scroll 事件的触发频率(浏览器一帧只执行一次),避免滚动太猛时事件堆积如果你愿意花点时间重构,IntersectionObserver 是更优雅的解法:
HTML 里在列表最后加个:
这个方案的优势是:
- 不依赖滚动位置计算,避免
scrollHeight赋值延迟、滚动边界抖动等问题- 浏览器原生支持,性能更好
- 不会因为用户手抖滚多次而重复触发,因为 IntersectionObserver 的回调是“状态驱动”的,只有当目标元素从“不可见”变成“可见”时才触发一次
最后说个容易被忽略的细节:如果你的
fetchData是用async/await写的,别忘了在函数开头就设锁,否则 async 函数的调用是同步的,但执行是异步的,锁可能没及时生效:你之前的问题,大概率是:
1. 用了防抖(应该用节流)
2. 锁没加在正确时机(应该在判断条件后立刻锁)
3. 没处理请求失败的情况(导致锁卡死)
按上面改完,基本不会再出现“滚一下发三枪”的情况了。