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

Designer°晓曼 阅读 2

我用 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;
};
我来解答 赞 1 收藏
二维码
手机扫码查看
1 条解答
西门柯依
这个问题很常见,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