前端请求缓存写了,同一个接口为什么还是打了十几次?

打开某个业务页,DevTools → Network 一刷新:同一个「拉全量选项」的接口连着出现十几次。参数几乎一样,响应也几乎一样。第一反应通常是:「谁又在乱请求?」

顺着调用链往下翻,却常常发现更尴尬的事实------调用方都在走同一个带缓存的 Store 方法。缓存写了,请求还是炸了。这篇文章讲这类问题怎么排查,以及一个只改十几行、却能管住整站的修法。


页面在干什么

中后台里很常见:搜索区有几个下拉(部门、分类、区域......),页面底部又常驻挂着一堆业务弹窗------新建、编辑、流转......弹窗里嵌着同样的选择器。

公共选择器的写法也很常见:挂载时自己拉数。

js 复制代码
onMounted(() => {
  getOptions()
})

const getOptions = async () => {
  const list = await shares.getSharedList()
  options.value = toTree(list)
}

父页面不必关心数据从哪来,选择器可复用,看起来很干净。问题也藏在这份「干净」里:首屏一挂载,选择器实例的数量,就是潜在请求次数的上限。


「我们明明有缓存」

Store 里已经做过「缓存为空才发请求」:

js 复制代码
const getSharedList = async () => {
  if (sharedList.value.length == 0) {
    const res = await axios.get('/api/xxx/list-all')
    sharedList.value = res.data.data
  }
  return sharedList.value
}

直觉上这很合理:第一次拉,之后复用。线上却不买账------首屏仍然 N 次并发。

关键不在「有没有缓存」,而在缓存生效的时机

把时间线摊开:

  1. Select_1 进入方法,length === 0,发起请求 A
  2. Select_2 几乎同时进入,请求 A 还没回来,length 仍是 0,发起请求 B
  3. ......直到 Select_N
  4. 若干毫秒后,请求陆续返回,缓存终于有值
  5. 之后的调用才真正命中缓存

也就是说:if (length == 0) 只能挡住已经写完缓存之后的调用,挡不住**飞行中(in-flight)**的并发。这不是 Vue、也不是 Pinia 的锅,是异步世界里经典的竞态。

还有一个加重项:个别业务组件绕过 Store,自己再 axios 一遍同一接口。Store 缓存再正确,也护不住这条旁路。


容易走偏的三种改法

排查时,桌上通常会摆出这些方案:

1. 父页面统一请求,props 往下传

能治,但要改所有选择器和所有使用方,和「选择器自取数」的既有模式对着干,改动面大。

2. 弹窗全部改成 v-if 懒挂载

首屏更轻,是好优化,却不是根因解。而且要处理 ref.open() 一类时序,一不小心就变成「点了没反应」。

3. 给选择器加本地防抖 / 本地缓存

每个实例一份本地状态,挡不住「多个实例同时首启」。

真正该动的是:共享数据入口本身,要能处理并发。


修法:In-flight Promise 单飞

思路很老,也很稳------single-flight:同一时刻,同一资源,只允许一架飞机起飞;后来的乘客都坐这一架。

js 复制代码
let sharedListPromise = null

const getSharedList = async () => {
  if (sharedList.value.length > 0) {
    return sharedList.value
  }

  if (!sharedListPromise) {
    sharedListPromise = (async () => {
      const res = await axios.get('/api/xxx/list-all')
      if (res.data.code === 0) {
        sharedList.value = res.data.data
      }
      return sharedList.value
    })().finally(() => {
      sharedListPromise = null // 无论成败都释放锁;失败时下次可再请求,成功时已有缓存不会再进这里
    })
  }

  return sharedListPromise
}

三层判断各管一段:

条件 行为
已有缓存 同步返回,零请求
请求飞行中 复用同一个 Promise
都没有 发起唯一一次请求

组件侧不用改 :仍然 onMountedgetSharedList()。多个选择器一起喊,Store 只放行一枪。

同一模式套到其它共享列表后,Network 里对应接口从「刷屏」变成「各一次」。旁路直调的组件,也改回走 Store,避免再开小灶。


为什么这个改动划算

  • 改动集中:修 Store 一处,全站同类选择器一起受益。
  • 不破坏现有组件契约:选择器继续自治,页面不用为了去重做 props 钻井。
  • 锁会释放、失败可再试finally 无论成败都会清空 Promise;成功后靠缓存挡重复,失败后下次还能再请求。
  • 语义清晰:缓存管「结果」,Promise 管「过程」,职责分开。

它治的是「共享只读列表被多组件同时预热」这类问题。若接口带强参数、结果不可共享,或必须每次强刷,不要硬套。


一个值得记住的判断标准

下次再看到「明明做了缓存还是重复请求」,先问一句:

这些调用,是在第一次响应返回之后 发生的,还是在第一次请求还没回来时一起冲进来的?

如果是后者,你缺的不是又一层 if,而是对 in-flight 状态的表达。用一个进行中的 Promise(或请求锁、或 dedupe 中间件)把它说清楚,往往比重构半个页面更有效。

中后台里,部门、字典、分类、组织树这类「几乎全局、几乎只读」的数据到处都是。它们适合集中缓存,也最容易在「组件自治 + 一页堆多个实例」的组合拳下被打穿。把单飞写进共享入口,是一次很小、但很耐用的防御。


小结

  • 重复请求的元凶,常常不是「没用缓存」,而是「缓存判断赢不了并发」。
  • length == 0 只覆盖完成后的世界;飞行中的世界需要 Promise 去重。
  • 优先修共享数据入口,而不是先推翻选择器自治或整页懒挂载。
  • 统一走 Store,禁止业务组件对同一 list-all 开旁路。

缓存负责记住答案;单飞负责保证,同一时间只有一个人去问问题。

相关推荐
2401_894915533 小时前
GEO 优化源码全解析:从搜索引擎到 AI 引擎的底层改写逻辑
java·服务器·前端·数据库·人工智能·分布式·搜索引擎
Profile排查笔记8 小时前
指纹浏览器哪个好?从 Profile、代理、权限和自动化能力判断是否适合
前端·人工智能·后端·自动化
lzhdim9 小时前
12、JavaScript常见的内存泄露问题 - JavaScript学习系列文章
开发语言·前端·javascript·学习·ecmascript
denggun123459 小时前
yield
前端·数据库·python
前端snow9 小时前
ai agent -- prompt汇总
前端
muddjsv10 小时前
前端性能优化实战:从加载到渲染的全面提速指南
前端·性能优化
deli00700711 小时前
JSON 树形查看器:粘贴即解析,折叠展开一目了然
前端·数据库·json·ai编程
guanguan0_011 小时前
PageProxy:页面维度接口地图 + 一键场景切换
前端·ai编程·请求代理·proxy工具
liangshanbo121511 小时前
虚拟列表深度面试题整理
java·开发语言·前端
IT_陈寒12 小时前
Redis内存警告竟是因为这个不起眼的配置项
前端·人工智能·后端