景泰拾伍.
首页 / 前端开发

Vue 3 列表筛选:旧请求为什么会覆盖新结果

Vue 3 / 异步请求   ·   整理于 2026-10-30

先搜索“agent”,再快速改成“agent ui”。如果第一次请求更慢,最终页面可能显示“agent”的结果,而输入框已经是“agent ui”。这是请求竞争,不是响应式失效。

防抖只减少次数,不能保证顺序

即便加了 300ms 防抖,已经发出的请求仍可能乱序返回。解决问题需要明确:只有当前这次请求可以更新 rows、error 和 loading。

下面的组合适用于 Vue 3 的 setup 环境。取消旧请求用于减少工作量,请求序号用于阻止过期结果写入。后者也能覆盖请求库不支持取消、或响应已经返回的情况。

import { ref, onBeforeUnmount } from 'vue';

const rows = ref<unknown[]>([]);
const loading = ref(false);
const error = ref('');
let sequence = 0;
let controller: AbortController | undefined;

async function load(query: string, page: number) {
  const id = ++sequence;
  controller?.abort();
  const current = new AbortController();
  controller = current;
  loading.value = true;
  error.value = '';

  try {
    const params = new URLSearchParams({ q: query, page: String(page) });
    const res = await fetch('/api/items?' + params, {
      signal: current.signal
    });
    if (!res.ok) throw new Error('HTTP ' + res.status);
    const data = await res.json();
    if (id !== sequence) return;
    rows.value = data.items;
  } catch (e) {
    if (id !== sequence || current.signal.aborted) return;
    error.value = e instanceof Error ? e.message : '加载失败';
  } finally {
    if (id === sequence) loading.value = false;
  }
}

onBeforeUnmount(() => {
  ++sequence;
  controller?.abort();
});

别漏掉 finally 中的 loading

只保护 rows 还不够。旧请求的 finally 先执行,会让新请求仍在加载时 loading 变成 false。错误提示也一样,过期请求的失败不应该盖住当前结果。

筛选条件变化时先重置页码

用户在第 8 页修改关键词,新条件可能只有 2 页。一次提交里把 page 重置为 1,再发出请求。不要让一个 watcher 修改页码、另一个 watcher 自动加载,最后触发两次请求。

将输入状态与已提交条件分开通常更清晰:输入框修改 draft,点击查询或防抖结束时生成一个完整的条件快照。缓存键也必须包含关键词、排序、分页,以及会影响结果的租户或权限范围。

验证不需要靠手速

在开发工具里让第一次请求延迟两秒、第二次延迟两百毫秒。第二次完成后,列表应该持续显示第二次结果;第一次随后成功或失败,都不应影响当前状态。

← 返回文章列表