Vue 3 列表筛选:旧请求为什么会覆盖新结果
先搜索“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,点击查询或防抖结束时生成一个完整的条件快照。缓存键也必须包含关键词、排序、分页,以及会影响结果的租户或权限范围。
验证不需要靠手速
在开发工具里让第一次请求延迟两秒、第二次延迟两百毫秒。第二次完成后,列表应该持续显示第二次结果;第一次随后成功或失败,都不应影响当前状态。