长列表分页加载时如何避免重复请求和数据错乱?

UP主~艺菲 阅读 41

我在做商品列表的分页加载,每次滚动到底部就调用接口拉下一页数据。但有时候网络慢,用户快速滚动会触发多次请求,导致数据重复或者顺序错乱,咋办?

我试过加个 loading 锁:if (loading) return;,但偶尔还是会出现重复项,特别是用户快速上下滚动的时候。

const loadMore = async () => {
  if (loading || !hasMore) return;
  loading = true;
  const res = await fetch(<code>/api/items?page=${page}</code>);
  items.push(...res.data);
  page++;
  loading = false;
};
我来解答 赞 6 收藏
二维码
手机扫码查看
2 条解答
FSD-国曼
啊,这个问题我遇到过很多次,确实烦人。你那个loading锁的思路是对的,但实现还不够完善。我来分享几个优化点:

首先,除了loading状态,还应该加个abortController来取消未完成的请求:

let controller = null;
const loadMore = async () => {
if (loading || !hasMore) return;

// 取消之前的未完成请求
if (controller) controller.abort();

loading = true;
controller = new AbortController();

try {
const res = await fetch(/api/items?page=${page}, {
signal: controller.signal
});
items.push(...res.data);
page++;
} catch (err) {
if (err.name !== 'AbortError') {
console.error('加载失败', err);
}
} finally {
loading = false;
controller = null;
}
};


其次,为了防止数据错乱,可以改用不可变更新方式,而不是直接push:

// 替换原来的items.push
items = [...items, ...res.data];


最后,建议加个防抖,比如用lodash的debounce或者自己简单实现:

const debouncedLoad = debounce(loadMore, 300);
// 滚动事件里调用debouncedLoad


这样三管齐下基本就能解决问题了:取消未完成请求 + 不可变更新 + 防抖。我项目里都这么搞,再没出现过重复数据的问题。

哦对了,如果你们用React的话,可以把这些逻辑封装成自定义hook,复用起来更方便。
点赞
2026-03-09 16:06
俊俊~
俊俊~ Lv1
这问题太常见了,我之前也被坑过。你那个loading锁其实不够,我给你说几个关键点:

第一,前端这边需要双重保险。除了loading状态,还要加个lastRequestId来标记最近一次请求。这样就算用户疯狂滑动,也能保证只处理最后一次请求的结果:

let lastRequestId = 0;
const loadMore = async () => {
if (loading || !hasMore) return;

loading = true;
const currentRequestId = ++lastRequestId;

try {
const res = await fetch(/api/items?page=${page});
// 只处理最新请求的结果
if (currentRequestId === lastRequestId) {
items.push(...res.data);
page++;
}
} finally {
loading = false;
}
};


第二,服务端最好也做防重处理。给分页接口加个timestamp参数,超过5秒的旧请求直接拒绝。这样能避免网络延迟导致的乱序问题。

第三,实在不放心的话,可以在合并数据前做去重判断。比如商品列表可以用id检查是否已存在。不过这个算是兜底方案了,前面两个处理好基本够用。

还有个细节,page++最好放在数据合并成功之后,避免请求失败时页码错位。
点赞 3
2026-03-06 02:09