长列表分页加载时怎么避免重复请求和数据错乱?
我用 Intersection Observer 做滚动分页加载,但快速滚动时经常触发多次请求,导致后一页的数据比前一页先回来,顺序全乱了。
试过加 loading 锁:if (loading) return;,但还是偶尔出现重复项或者跳页。有没有更稳妥的做法?
这是我的加载逻辑:
const loadMore = async () => {
if (loading || !hasMore) return;
loading = true;
const res = await fetch(<code>/api/items?page=${page + 1}</code>);
const data = await res.json();
items.push(...data.items);
page += 1;
hasMore = data.hasMore;
loading = false;
};
首先,确保在请求开始时增加 loading 标志,并且在请求结束时再把它设为 false。这个你已经在做了。
其次,每次请求回来之后,先检查当前的 page 是否和请求时的 page 一致。如果不一致,说明用户已经滚动到了其他页面,这时候就不应该更新当前的数据列表了。
最后,为了避免数据错乱,可以在每次请求回来时,用新的数据替换掉旧的分页数据,而不是简单地追加。
可以参考下面的修改:
这里的关键是
requestPage和page的对比,确保只处理与当前显示页匹配的数据。这样可以有效防止数据错乱和重复请求的问题。调试看看,应该能改善一下体验。核心思路是:要么强制顺序执行,要么取消过期请求。
最稳妥的做法是用一个请求队列,每次 loadMore 不是直接发请求,而是 push 到队列里,然后串行处理:
这样保证请求按触发顺序依次执行,不会出现顺序错乱。
另一个更狠的做法是用 AbortController,直接取消还在路上的旧请求:
不过 AbortController 这种适合「只保留最新请求」的场景,如果你想保留所有数据还是用队列方案更稳。
还有一个容易漏的点:Intersection Observer 本身也可能触发多次。你需要确保在触发时做下防抖,或者用一个标记表示当前是否已经建立了观察:
其实 loading 锁配合队列基本就能解决 90% 的问题了,代码改起来也不大,优先试试队列方案。