Loading状态怎么避免闪烁或重复请求?

东方保霞 阅读 21

我在用 Vue 做一个搜索功能,每次输入关键词就发请求,但发现如果用户打字快,会触发多次请求,而且 Loading 状态一闪一闪的特别难受。我试过加防抖,但有时候还是会出现上一次请求还没结束,新的请求又来了,导致界面状态错乱。

下面是我现在的代码,loading 是在请求开始设为 true,结束设为 false,但这样处理好像不够稳:

<template>
  <div>
    <input v-model="keyword" @input="debouncedSearch" />
    <div v-if="loading">加载中...</div>
    <ul><li v-for="item in results" :key="item.id">{{ item.name }}</li></ul>
  </div>
</template>

<script>
export default {
  data() {
    return { keyword: '', loading: false, results: [] }
  },
  methods: {
    async search() {
      this.loading = true
      const res = await api.search(this.keyword)
      this.results = res.data
      this.loading = false
    }
  },
  created() {
    this.debouncedSearch = _.debounce(this.search, 300)
  }
}
</script>
我来解答 赞 9 收藏
二维码
手机扫码查看
2 条解答
令狐红彦
这个问题其实很常见,核心问题在于:你只用防抖,但没管请求的“时序”。防抖能减少触发频率,但不能保证请求按顺序返回,所以会出现“后发的请求先回来,把前面的结果覆盖了,Loading 也跟着乱跳”的情况。

按照规范,得加一层“请求上下文”或“版本号”机制,让每个请求带上自己的标识,只处理最新那次请求的结果。最简单的做法是用一个 requestIdcurrentQuery 字段来标记当前有效的请求。

比如这样改:

data() {
return {
keyword: '',
loading: false,
results: [],
currentQuery: '' // 标记当前有效的搜索关键词
}
},
methods: {
async search() {
// 先记录当前请求对应的关键词
const query = this.keyword
this.currentQuery = query
this.loading = true

try {
const res = await api.search(query)

// 关键:只有当当前结果对应的关键词还是这次请求的,才更新
if (this.currentQuery !== query) return

this.results = res.data
} finally {
// 只在最后一次有效请求结束后才关 loading
if (this.currentQuery === query) {
this.loading = false
}
}
}
}


防抖还是得保留(比如 debounce(300)),但防抖只是减少不必要的请求,真正防止闪烁的是这个 currentQuery 校验——它确保只有“最新一次输入”对应的请求结果会被采纳,旧请求就算晚回来了也直接被丢弃。

另外注意:finally 里不能无脑设 loading = false,必须判断是不是“当前有效请求”结束,否则上一个慢请求结束时会错误地关掉 loading。

我之前踩过这坑,以为加个防抖就万事大吉,结果测试时一连打“abc”“abcd”“abcde”,前两个请求慢悠悠回来,界面先显示“abc”的结果,然后突然切回“abcde”的,Loading 也闪得像霓虹灯……后来加了这个判断才消停。
点赞 3
2026-02-27 15:04
Tr° 万华
你这个问题其实很常见,核心是两个点:一是请求并发导致状态混乱,二是 Loading 状态没跟上「当前有效请求」。

先说结论:别用全局 loading,要给每次请求打个「请求序号」或者「token」,只响应最新那次请求的结果,老请求回来就直接丢掉。

拿去改改这个版本:

<script>
export default {
data() {
return {
keyword: '',
loading: false,
results: [],
currentRequestId: 0 // 新增:追踪当前请求
}
},
methods: {
async search() {
this.loading = true
const requestId = ++this.currentRequestId // 每次新请求自增
try {
const res = await api.search(this.keyword)
// 关键:只处理当前最新的请求返回
if (requestId === this.currentRequestId) {
this.results = res.data
}
} catch (e) {
// 错误也要判断是不是当前请求,避免老请求错误覆盖新请求状态
if (requestId === this.currentRequestId) {
console.error(e)
}
} finally {
// 注意:这里别直接设 loading=false!要等最后一次请求完成再关
// 所以改成:如果当前 requestId 还是它,才关 loading
if (requestId === this.currentRequestId) {
this.loading = false
}
}
}
},
created() {
this.debouncedSearch = _.debounce(() => {
this.search()
}, 300)
}
}
</script>


另外补充一点:防抖时间别太短,300ms 对搜索来说可能还是有点快,建议 500ms 起步,除非你后端真能扛住高频请求。

你要是图省事,也可以用 axios 的取消令牌,或者直接用 AbortController 把旧请求 cancel 掉,不过上面这种「请求 ID」方式简单粗暴好理解,改起来也快,拿去试试。
点赞
2026-02-26 09:04