·更新于 2026年10月25日· 9 次浏览 Nuxt3Vue3AbortController竞态BUG解决

Nuxt 3 搜索联想越输越乱:我用请求序列号和 AbortController 干掉旧响应

搜索联想框里,旧请求晚到会覆盖新结果。我用 AbortController 取消旧请求,再用请求序列号校验结果归属,把列表、错误和加载态收成一套防竞态方案。

Nuxt 3 搜索联想越输越乱:我用请求序列号和 AbortController 干掉旧响应 封面
ZHOUYI

作者

ZHOUYI

给站内搜索加联想时,我遇到一个很典型的场景:用户刚输入「支付」,又快速改成「支付宝」,下拉列表却显示旧词的结果。一开始我以为是接口慢,盯了半天才发现,真正的问题是旧响应晚到,把新结果覆盖了。

旧响应为什么会覆盖新结果

每敲一个字就发一次请求,网络耗时又不稳定,先发后至很常见。如果拿到数据就直接赋值,旧关键词的结果会盖掉新关键词。只做防抖也不够:防抖能减少请求,但已经发出去的旧请求仍可能回来。

取消请求、序列号校验和加载态收敛

  • 发新请求前,用 AbortController 中止上一次未完成的请求,减少无意义响应。
  • 给每次请求分配递增序列号,只有最新序列号的结果才允许写入页面。
  • 加载态和错误态也按最新序列号收尾,避免旧请求把新请求的状态弄乱。
ts
const seq = ref(0)
let controller: AbortController | null = null
async function suggest(keyword: string) {
  controller?.abort()
  controller = new AbortController()
  const current = ++seq.value
  loading.value = true
  try {
    const res = await $fetch('/api/suggest', { query: { q: keyword }, signal: controller.signal })
    if (current === seq.value) list.value = res.items
  } catch (e) {
    if (!controller?.signal.aborted && current === seq.value) error.value = '查询失败'
  } finally { if (current === seq.value) loading.value = false }
}

这里最关键的是判断 current 是否还等于 seq。AbortController 负责让浏览器尽早断开,序列号负责兜底:即使取消失败、响应还是回来了,也不会污染当前列表。用普通 fetch 或封装请求也一样,核心是把 signal 传进去,具体兼容以官方文档为准。

Nuxt 3 里怎么放这段逻辑

我通常把联想放在客户端组件里,本地维护 keyword、list、loading 和 error,不把每次按键都塞进 useAsyncData。服务端只提供轻量查询接口,并对关键词做最短长度、转义和简单限流。联想接口尽量别做长缓存,避免不同关键词互相串味。

上线前我还补了一个细节:输入清空时立刻取消请求并重置列表,避免用户删完字后还看到旧联想。加载态也不要每个请求都管,只让最新请求更新 UI,下拉框就不会一闪一闪。

“这类问题本质上不是接口优化,而是状态归属:每个异步结果都要知道自己属于哪一次输入。”

Guestbook

留言

若这篇文章也曾停在你心上,留一行字吧,我会看的。

先确认一下邮箱

多这一步,是为了少些机器骚扰,也让我能更踏实地读每一条真人写下的字。验证码 10 分钟内有效;同一邮箱 60 秒内只能发一封。

本文留言

共 0 条已加载 0 条

这里还静悄悄的——愿意做第一个留下脚印的人吗?