← 返回文章列表
李奕锦的个人网站所属专题:侦探式排查实录

Tab 切一下,接口调两次?记一次 Vue KeepAlive 引发的"鬼打墙"排查实录

更新于 2025-12-21年份:2025字数:2,200阅读时长:7 分钟

Tab 切换时接口被调用两次,导致弱网下加载翻倍。本文从现象出发,剖析 KeepAlive + 动态组件的组合在 Vue 中的行为,给出 v-show + key 的根治方案,并总结生命周期与父子职责的避坑指南。

上午 10 点,刚泡好的咖啡还没凉,项目群里突然炸锅:"兄弟,怎么点一下 Tab 切个页面,接口发了两次?用户都在吐槽加载慢得像蜗牛!"

我心里一惊,切回 Chrome 的 Network 面板,好家伙——那熟悉的 API 请求像双胞胎一样并排出现。盯着这两行一模一样的请求,我心里只有一句话:这锅,看来是背定了。

这种"看起来简单、实则藏了不少 Vue 底层细节"的 Bug 挺有意思。修完线上事故的同时,也是把组件生命周期、缓存机制这些八股文落地变现的机会。

下面把"排查到根治"的过程拆开说——不整虚的,只讲干货和避坑。

案发现场

一、案发现场:一次点击,双倍快乐?

场景其实很常见:首页有两个 Tab,一个是"门票(Ticket)",一个是"生活(Life)"。原本的代码逻辑是用动态组件来切换的:

<!-- 父组件 index.vue -->
<KeepAlive>
  <component :is="activeTab === 'ticket' ? Ticket : Life" />
</KeepAlive>

只要一点 Tab,控制台里的接口请求就跟不要钱一样发了两次。在弱网环境下,Loading 转圈的时间直接翻倍,用户体验极差。

第一反应:是不是子组件里写了重复逻辑?

打开 Ticket.vue 和 Life.vue,一眼看到了"罪魁祸首":

onMounted(() => {
  init()  // 这里的 init() 里包含了 onLoad() 发请求
})

破案了?并没有。

每个组件挂载时拉数据没毛病。但问题是:明明加了 KeepAlive,为啥切回来还会重新触发 onMounted?说好的缓存呢?

抽丝剥茧

二、抽丝剥茧:KeepAlive + 动态组件的"致命组合"

为了验证猜想,我先把 KeepAlive 去掉,结果发现——还是两次。这说明问题不在缓存失效,而在组件的创建机制上。

稍微翻了一下 Vue 的源码(对,被逼急了是真的会看源码的),真相其实很简单:

当你在用 `<component :is="condition ? A : B" />` 时,Vue 面对的是两个完全不同类型的组件。

从 A 切到 B:Vue 认为是"拆迁队进场",把 A 铲平(Unmount),原地盖个 B(Mount)。

即便包了 KeepAlive,它缓存的是实例。但如果你在切换逻辑里让组件频繁销毁重建,或者触发了某些导致 key 变化的机制,KeepAlive 也救不了你。它就像个冰箱,你今天放披萨,明天放寿司,冰箱只能说:"哥们,你换菜了,我只能重新做。"

再对比一下 v-show:v-show 只是 CSS 层面的 display: none。组件一开始就全部挂载好了(onMounted 触发一次)。切换 Tab 只是"拉窗帘"和"开窗帘",根本不会触发生命周期钩子。

结论是:只有两个固定 Tab 频繁切换时,动态组件 + KeepAlive 反而把简单问题搞复杂了,v-if / v-show 才是正解。

根治方案

三、根治方案:v-show + Key 的"移花接木"

既然找到了病灶,药方就得下得准。最终方案只有两招,却刀刀致命:

1. 父组件:用 v-show 控显示,用 Key 控刷新

别让组件"自作主张"去刷新,把控制权收回到父组件手里。

<!-- index.vue -->
<script setup>
const refreshKey = ref(0) // 这是一个核心的"遥控器"
const homeStore = useHomeStore()

// 监听业务数据的变化(比如切换了城市/目的地)
watch(() => homeStore.getDestinationitem()?.socialRegionId, (newId, oldId) => {
  if (newId && oldId && newId !== oldId) {
    // 只有目的地真的变了,才通知子组件:"嘿,你需要重置了"
    refreshKey.value++
  }
})
</script>

<template>
  <!-- 这里的 Key 才是灵魂! -->
  <!-- Key 没变时,v-show 切换只是改 CSS,0 开销 -->
  <!-- Key 变了(换城市了),Vue 才会真正销毁重建组件,触发新请求 -->
  <Ticket
    v-show="activeTab === 'ticket'"
    :key="`ticket-${refreshKey}`"
  />
  <Life
    v-show="activeTab === 'life'"
    :key="`life-${refreshKey}`"
  />
</template>

2. 子组件:彻底"躺平"

子组件要像个乖宝宝,不要自己搞事情:删除 onMounted 里的主动请求逻辑(或者只留第一次初始化的兜底)。移除那些花里胡哨的 document.addEventListener('visibilitychange')。这玩意是监听浏览器 Tab 切后台的,拿来监听组件 Tab 切换简直是高射炮打蚊子——打不准还费劲。

效果立竿见影:改完上线后,Tab 切换变成了纯 CSS 操作,丝般顺滑。只有在真正切换"城市/目的地"时,才会触发数据刷新。接口调用量直接砍半,产品经理看完演示,反手就是一个点赞。

碎碎念

四、踩坑后的"碎碎念"

这次修 Bug,与其说是改代码,不如说是对 Vue 机制的一次再思考。几个血泪教训分享给大家:

KeepAlive 不是万能胶:它适合"列表页 -> 详情页"这种保留状态的场景。但在"Tab A <-> Tab B"这种同级高频切换场景,尤其是组件差异巨大时,盲目上 KeepAlive 容易把自己绕进去。

生命周期不是摆设,但也别乱用:onMounted 不等于"每次用户看见时执行"。真正需要"每次可见都刷新"的场景,请考虑 onActivated(配合 KeepAlive),或者像上面那样用 key 强行重置。

父子职责要分清:别让每个子组件都去监听全局 Store。父组件才是"指挥官",子组件只负责渲染。父组件通过 props 或 key 告诉子组件"该干活了",代码逻辑才不会乱成一锅粥。

写在最后

写在最后

前端有意思的地方,往往藏在这些不起眼的 Bug 里。下次再遇到 Tab 切换、KeepAlive、动态组件这套组合拳,别慌。喝口咖啡,想想"冰箱"和"窗帘"的例子——读懂了框架的脾气,比死磕 API 管用。

阅读时长:7 分钟


文档信息

版权声明:自由转载-非商用-非衍生-保持署名(CC BY-NC-ND 3.0)

原文链接:https://yijinlee.com/share-future/article-9

作者:李奕锦

商业用途或修改衍生请联系授权。


TL;DR

  • 动态组件 + KeepAlive:切换 Tab 时 Vue 会销毁/重建组件,onMounted 仍会执行,导致重复请求。
  • 根治方案:v-show 控显示 + key 控刷新;只有业务数据变化时改 key,触发子组件重建。
  • 父子职责:父组件通过 props/key 通知"该干活了",子组件不监听全局 Store,避免乱成一锅粥。
Tags:Vue 3KeepAlive动态组件v-show生命周期

该专题下的阅读路径

现象分析 → 根因定位 → 解决方案复盘