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

Designer°晓曼 阅读 55

我用 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;
};
我来解答 赞 9 收藏
二维码
手机扫码查看
2 条解答
夏侯文阁
遇到这个问题挺头疼的,不过解决起来也有方法。你提到的 loading 锁其实是个不错的思路,但可能还需要做些调整来避免数据错乱和重复请求。我们可以考虑在每次请求回来后,检查当前页面是否已经改变,以及如何处理新来的数据。

首先,确保在请求开始时增加 loading 标志,并且在请求结束时再把它设为 false。这个你已经在做了。

其次,每次请求回来之后,先检查当前的 page 是否和请求时的 page 一致。如果不一致,说明用户已经滚动到了其他页面,这时候就不应该更新当前的数据列表了。

最后,为了避免数据错乱,可以在每次请求回来时,用新的数据替换掉旧的分页数据,而不是简单地追加。

可以参考下面的修改:

let currentPage = 0;

const loadMore = async () => {
if (loading || !hasMore) return;
const requestPage = page + 1; // 记录请求时的页码
loading = true;
try {
const res = await fetch(/api/items?page=${requestPage});
const data = await res.json();
if (requestPage === page) { // 检查页码是否一致
items = [...items, ...data.items]; // 替换或追加数据,视情况而定
page += 1;
hasMore = data.hasMore;
}
} catch (error) {
console.error('Error fetching data:', error);
} finally {
loading = false;
}
};


这里的关键是 requestPagepage 的对比,确保只处理与当前显示页匹配的数据。这样可以有效防止数据错乱和重复请求的问题。调试看看,应该能改善一下体验。
点赞
2026-03-24 03:03
西门柯依
这个问题很常见,loading 锁只能防止并发请求,但解决不了请求返回顺序乱套的问题。后发的请求先回来,数据就全乱了。

核心思路是:要么强制顺序执行,要么取消过期请求。

最稳妥的做法是用一个请求队列,每次 loadMore 不是直接发请求,而是 push 到队列里,然后串行处理:

const queue = [];
let isProcessing = false;

const processQueue = async () => {
if (isProcessing || queue.length === 0) return;
isProcessing = true;

while (queue.length > 0) {
const { page, resolve } = queue.shift();
try {
const res = await fetch(/api/items?page=${page});
const data = await res.json();
resolve(data);
} catch (e) {
resolve(null);
}
}

isProcessing = false;
};

const loadMore = () => {
return new Promise((resolve) => {
queue.push({ page: page + 1, resolve });
processQueue();
});
};

// 使用时
loadMore().then(data => {
if (!data) return;
items.push(...data.items);
page += 1;
hasMore = data.hasMore;
});


这样保证请求按触发顺序依次执行,不会出现顺序错乱。

另一个更狠的做法是用 AbortController,直接取消还在路上的旧请求:

let abortController = null;

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

// 取消上一个还没返回的请求
if (abortController) {
abortController.abort();
}
abortController = new AbortController();

loading = true;
try {
const res = await fetch(/api/items?page=${page + 1}, {
signal: abortController.signal
});
const data = await res.json();

// 防止竞态:检查 page 是否还是我们期望的值
// 如果期间有其他请求触发,这里可以加个版本号之类的
items.push(...data.items);
page += 1;
hasMore = data.hasMore;
} catch (e) {
if (e.name !== 'AbortError') {
console.error(e);
}
}
loading = false;
};


不过 AbortController 这种适合「只保留最新请求」的场景,如果你想保留所有数据还是用队列方案更稳。

还有一个容易漏的点:Intersection Observer 本身也可能触发多次。你需要确保在触发时做下防抖,或者用一个标记表示当前是否已经建立了观察:

const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !loading && hasMore) {
loadMore();
}
}, { threshold: 0.1 });

observer.observe(element);


其实 loading 锁配合队列基本就能解决 90% 的问题了,代码改起来也不大,优先试试队列方案。
点赞
2026-03-20 04:04