Vue3 Keep-Alive清不掉怎么办?3种精准清单个/批量清除缓存的场景化方案
日常开发Vue3项目时,有没有遇见过这种头疼的情况?本来想让列表页翻页、筛选过的状态,下次返回还要还原,就用了Keep-Alive,结果有时候不需要还原的场景——比如从A页面新增一条新列表页→B页详情页→修改数据后返回A,新增/修改的内容应该刷新,但Keep-Alive死死把旧状态存着;或者从首页跳转到C、D、E这三个页面后,再从E跳回C、D、E任意一个,要清掉前一个;甚至想直接清掉所有缓存池里的东西?这时候硬刷新整个页面又破坏体验,又回到原生APP切换丝滑的初衷了。 其实Vue3的Keep-Alive确实比Vue2多了不少特性,但很多同学还在用旧思路套,或者没搞懂它的底层缓存逻辑,才会踩清不掉的坑,今天就从「底层逻辑」开始讲起,然后分三个高频场景,给你能直接复制粘贴、还能应对项目复杂情况的方案,保证下次不会再卡壳。
先搞懂3个核心:为什么你的Keep-Alive清不掉?
很多人上来就找代码片段,复制了没用,大概率是没踩中这三个关键点,先补补基础,后面的方案才能用得顺手,甚至自己能改。
Keep-Alive在Vue3里变了啥?
最明显的是写法变了:Vue2里是用<keep-alive>标签直接包裹<router-view>或者动态组件<component :is="xxx">;Vue3如果用的是<router-view v-slot="{ Component }>写法的话,必须把<keep-alive>放在插槽里包裹<component>,比如标准的错误示范经常是直接把<keep-alive>加在<router-view>外面,这样根本不会生效,更别说清缓存了。
底层实现机制有没有变?核心还是LRU(最近最少使用)缓存策略没变,但Vue3里新增了几个更灵活的属性和生命周期钩子:max可以限制最大缓存数量,超过就自动清最久不用的;exclude可以动态绑定数组/正则/字符串,实时排除不需要缓存的组件;还有就是内部暴露出来的__v_cache这个属性(虽然是内部属性,但紧急情况可以用,但最好别硬来,后面讲)。
你的组件到底有没有被缓存?
很多时候你以为组件被缓存了,其实根本没进缓存池,怎么判断?
第一个方法是加生命周期钩子:被Keep-Alive缓存的组件,第一次创建会触发onMounted,之后切换只会触发onActivated,销毁触发onDeactivated,不会触发onUnmounted;没被缓存的话,每次切换都会onUnmounted然后重新onMounted,你可以在组件里加个console.log打印这几个钩子的名字,看看控制台输出什么。
第二个方法是看组件实例上的__v_keepAlive属性:如果是true,就进了;false就没进,这个比看钩子更直接,不用等触发。
为什么加了include/exclude还不管用?
第一个可能是组件的name属性没加,或者加错了,不管是Vue2还是Vue3,Keep-Alive的include/exclude都是靠组件的name(注意不是路由的name!这点踩坑的人最多!)来匹配的,你在setup里用<script setup>的话,默认没有name,必须显式加<script>标签补充name,或者用Vue3.2+新增的defineOptions:比如defineOptions({ name: 'ListPage' })。
第二个可能是动态组件的is属性绑定的不是同一个组件实例?不对,应该是同一个,但如果是动态路由匹配的组件,比如用了动态参数id,但同一个组件的不同路由参数,默认Keep-Alive会当成同一个组件来缓存,除非你加key在<component>上,这个key最好是路由的fullPath或者path + params,这样参数变了,组件会重新创建,自然就清缓存了——不过这不是精准清,是直接不用之前的,后面讲精准的。
第三个可能是缓存的组件有嵌套?比如你有个Layout组件被缓存了,Layout里的子组件没被缓存,或者反过来?这时候要看清到底是哪个组件需要清缓存,别搞错层级。
高频场景一:从详情页返回列表页,只清除当前这次修改过的列表页
这应该是最最常见的场景了吧?比如电商项目,你在商品列表页选了“销量优先”、翻到第3页,点进去商品详情页,修改了商品的收藏状态,或者从商品详情页加了商品到购物车,或者管理员后台,从用户列表页点进去用户详情页,修改了用户的昵称,然后返回列表页,这时候列表页应该:
- 如果是修改了单条数据的状态,比如收藏、昵称,列表页对应的数据要刷新,但页码、筛选条件可以保留;
- 如果是新增了单条数据,比如加了商品到购物车,新增了用户,列表页要跳回第一页,同时筛选条件可以保留,也可以清,看产品需求。
不管哪种需求,核心思路都是「在列表页加一个变量,控制返回列表页的时候触发的时候,判断要不要刷新数据,那怎么判断是从详情页返回的呢?怎么传这个要不要刷新的信号呢?
方案1.1:用路由守卫的meta配合Keep-Alive的activated钩子
这个方案是最稳的,不会用到内部属性,也不会破坏组件的其他逻辑。
第一步:在路由配置里加两个属性
在路由配置里,给列表页加一个
isCache属性(或者needRefresh,反过来也行),默认是true,就是需要缓存;给详情页加一个fromList属性?不对,应该是从详情页返回的时候,修改列表页的needRefresh为true,那怎么修改呢?可以用路由的全局后置守卫,或者路由的组件内守卫? 哦,更好的方法是用路由的beforeLeave守卫,在详情页里写:当离开详情页,跳转的目标路径是列表页的路径的时候,给目标路由的meta加一个临时的refresh属性,值为true。 等下,临时加meta会不会有问题?不会,因为路由的meta是响应式的吗?Vue3的vue-router4里,路由的meta是响应式的,但修改临时属性的话,其实可以用路由的params或者query,但params刷新页面会丢,query会显示在URL里,不美观,所以还是用meta的临时属性,或者用Vuex/Pinia?第二步:在详情页的beforeRouteLeave里设置信号
如果用
<script setup>的话,组件内的路由守卫需要从vue-router里导入onBeforeRouteLeave,然后写:import { useRouter, useRoute } from 'vue-router' const router = useRouter() const route = useRoute() onBeforeRouteLeave((to, from, next) => { // 判断目标路径是不是列表页的路径,这里可以用正则,也可以用to.name,假设列表页的name是List if (to.name === 'List') { // 给目标路由的meta加一个临时的refreshList属性 to.meta.refreshList = true } next() })第三步:在列表页的activated钩子读取信号
然后在列表页的
onActivated里判断这个信号:import { onActivated, ref } from 'vue' import { useRoute } from 'vue-router' const route = useRoute() const listData = ref([]) const getListData = () => { // 这里写你的获取列表数据的逻辑,比如调用API console.log('获取列表数据') // 假设获取成功后 listData.value = [1,2,3] } // 第一次进入的时候要获取数据 onMounted(() => { getListData() }) // 每次激活的时候判断要不要刷新 onActivated(() => { if (route.meta.refreshList) { // 先把临时属性删掉,避免下次返回又刷新 route.meta.refreshList = false // 这里可以根据产品需求,决定要不要重置页码、筛选条件 // 比如产品需求1:只刷新数据,保留页码、筛选条件 getListData() // 比如产品需求2:重置页码为1,筛选条件保留,然后刷新数据 // pageNum.value = 1 // getListData() } })这个方案是不是很简单?但是有没有问题?有,比如如果有多个页面都可以跳转到详情页,然后返回列表页?比如从首页→列表页→详情页,这时候没问题;如果从首页→详情页→列表页,这时候也会触发刷新,对吗?对,这时候就需要在详情页的beforeRouteLeave里再加一个判断,判断from的路径是不是列表页的路径?不对,应该是判断from的path是不是列表页的path?不,是判断from.name是不是List,如果是的话,才加refreshList,否则不加?
onBeforeRouteLeave((to, from, next) => { if (to.name === 'List' && from.name === 'Detail') { to.meta.refreshList = true } next() })哦对,这样就只有从详情页返回列表页的时候才会刷新,其他页面跳转到列表页的话,不会刷新,列表页还是保持之前的状态。
方案1.2:用Pinia/Vuex全局状态管理信号
如果项目里已经用了Pinia或者Vuex的话,这个方案也很方便,而且不用修改路由的meta,也不用担心临时属性的问题。
第一步:在Pinia里定义一个store
比如定义一个
cacheStore,里面有一个needRefreshList的状态,默认是false,还有一个修改状态的方法:// stores/cache.js import { defineStore } from 'pinia' export const useCacheStore = defineStore('cache', { state: () => ({ needRefreshList: false }), actions: { setNeedRefreshList(val) { this.needRefreshList = val } } })第二步:在详情页里修改状态
// Detail.vue import { useCacheStore } from '@/stores/cache' import { onBeforeRouteLeave } from 'vue-router' const cacheStore = useCacheStore() onBeforeRouteLeave((to, from, next) => { if (to.name === 'List' && from.name === 'Detail') { cacheStore.setNeedRefreshList(true) } next() })第三步:在列表页里读取状态
// List.vue import { useCacheStore } from '@/stores/cache' import { onActivated } from 'vue' const cacheStore = useCacheStore() onActivated(() => { if (cacheStore.needRefreshList) { cacheStore.setNeedRefreshList(false) getListData() } })这个方案的好处是,如果有多个列表页需要控制刷新的话,可以在store里加多个状态,比如
needRefreshUserList、needRefreshOrderList等等,不会混乱;而且如果是跨页面传递多个刷新信号的话,也更方便。方案1.3:用provide/inject传递信号
如果项目里没有用Pinia/Vuex的话,这个方案也可以,但不如前两个稳,因为provide/inject是跨组件的,层级要对,必须是父组件provide,子组件inject,或者反过来?不,provide/inject可以跨多层,但如果组件销毁了的话,inject会不会有问题?比如从列表页→详情页,这时候列表页是父组件,详情页是子组件,那可以在列表页provide一个`inject?或者反过来?哦,列表页是详情页的父组件,所以可以在列表页provide一个修改刷新方法,然后详情页inject这个方法,在离开详情页的时候调用这个方法。 不过这个方案的层级限制比较大,所以还是推荐前两个方案。
高频场景二:从E页面跳转到C/D/E任意一个,清掉前一个
比如有个Tab栏,有三个Tab:C(订单待付款)、D(订单待发货)、E(订单已完成),点击Tab切换的时候,要清掉前一个Tab的缓存,因为这三个Tab其实是同一个组件,只是路由参数或者query不同?不对,如果是同一个组件的话,加个key在<component>上,key用route.fullPath,参数变了,组件会重新创建,自然就不用之前的缓存了,但如果是三个不同的组件呢?比如C是OrderUnpaid,D是OrderUnshipped,E是OrderCompleted,这时候就需要精准清掉前一个组件的缓存了。
或者比如首页→C→D→E,这时候从E跳回C的话,要清掉D和E的缓存;从E跳回D的话,要清掉E的缓存;从C跳回首页的话,要清掉C的缓存?
这个场景的核心思路是「动态修改Keep-Alive的exclude/include数组」,因为Vue3里的exclude/include是响应式的,所以可以实时修改,实时生效。
方案2.1:用Pinia/Vuex全局管理exclude/include数组
还是推荐用Pinia/Vuex,因为要在Layout组件(就是包裹<keep-alive>的组件,或者App.vue?不对,包裹<keep-alive>的组件)里要用到这个数组,在其他组件里也要修改这个数组。
第一步:在Pinia里定义store
// stores/cache.js
import { defineStore } from 'pinia'
export const useCacheStore = defineStore('cache', {
state: () => ({
// 这里用include还是exclude?看哪个方便,比如用include的话,只有在数组里的组件才会被缓存;用exclude的话,不在数组里的组件才会被缓存,这里假设用include,因为更可控
cachedComponents: ['Home', 'List'] // 先把首页和列表页加进去,默认缓存
}),
actions: {
// 添加要缓存的组件
addCachedComponent(name) {
if (!this.cachedComponents.includes(name)) {
this.cachedComponents.push(name)
}
},
// 删除要缓存的组件
removeCachedComponent(name) {
const index = this.cachedComponents.indexOf(name)
if (index > -1) {
this.cachedComponents.splice(index, 1)
}
},
// 清空所有要缓存的组件
clearAllCachedComponents() {
this.cachedComponents = []
}
}
})
第二步:在包裹<keep-alive>的组件里绑定include
假设是App.vue,或者Layout.vue:
<!-- App.vue -->
<template>
<router-view v-slot="{ Component, route }">
<transition name="fade" mode="out-in">
<keep-alive :include="cacheStore.cachedComponents">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</template>
<script setup>
import { useCacheStore } from '@/stores/cache'
const cacheStore = useCacheStore()
</script>
注意这里加了key="route.fullPath",这样如果是同一个组件的不同路由参数,会重新创建,不会被当成同一个组件缓存。
第三步:在组件里修改cachedComponents数组
比如在C组件里:
<!-- C.vue -->
<script setup>
import { defineOptions } from 'vue'
import { useCacheStore } from '@/stores/cache'
import { onActivated, onBeforeRouteLeave } from 'vue-router'
// 必须显式加name,不然include匹配不到
defineOptions({ name: 'OrderUnpaid' })
const cacheStore = useCacheStore()
// 第一次进入或者重新创建的时候,添加到include数组里
onMounted(() => {
cacheStore.addCachedComponent('OrderUnpaid')
})
// 每次激活的时候,也要添加,因为可能之前被删除了
onActivated(() => {
cacheStore.addCachedComponent('OrderUnpaid')
})
// 离开的时候,判断目标路径是不是C、D、E,如果是的话,删除当前组件的name
onBeforeRouteLeave((to, from, next) => {
// 假设C、D、E的name分别是OrderUnpaid、OrderUnshipped、OrderCompleted
if (['OrderUnshipped', 'OrderCompleted'].includes(to.name)) {
cacheStore.removeCachedComponent('OrderUnpaid')
}
next()
})
</script>
然后在D组件里:
<!-- D.vue -->
<script setup>
import { defineOptions } from 'vue'
import { useCacheStore } from '@/stores/cache'
import { onActivated, onBeforeRouteLeave } from 'vue-router'
defineOptions({ name: 'OrderUnshipped' })
const cacheStore = useCacheStore()
onMounted(() => {
cacheStore.addCachedComponent('OrderUnshipped')
})
onActivated(() => {
cacheStore.addCachedComponent('OrderUnshipped')
})
onBeforeRouteLeave((to, from, next) => {
if (['OrderUnpaid', 'OrderCompleted'].includes(to.name)) {
cacheStore.removeCachedComponent('OrderUnshipped')
}
next()
})
</script>
E组件同理。 这样是不是就实现了从E跳转到C的话,清掉E的缓存;从E跳转到D的话,清掉E的缓存;从D跳转到C的话,清掉D的缓存;从C跳转到D的话,清掉C的缓存?对的,那如果从E跳转到首页的话,要不要清掉E的缓存?可以在onBeforeRouteLeave里再加一个判断,如果目标路径是首页的话,也删除当前组件的name,或者根据产品需求来。
方案2.2:不用Pinia/Vuex,用路由的meta结合全局后置守卫
如果项目里没有用Pinia/Vuex的话,这个方案也可以,但不如前一个稳,因为要在全局后置守卫里维护一个数组,还要注意响应式的问题。
第一步:在路由配置里加一个属性
比如给需要动态控制缓存的组件加一个dynamicCache属性,值为true。
第二步:在App.vue里定义一个响应式的数组
<!-- App.vue -->
<template>
<router-view v-slot="{ Component, route }">
<transition name="fade" mode="out-in">
<keep-alive :include="cachedComponents">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const cachedComponents = ref(['Home', 'List'])
// 全局后置守卫
router.afterEach((to, from) => {
// 如果目标路由的dynamicCache是true的话,添加到cachedComponents里
if (to.meta.dynamicCache) {
if (!cachedComponents.value.includes(to.name)) {
cachedComponents.value.push(to.name)
}
}
// 如果来源路由的dynamicCache是true的话,判断目标路由是不是其他的dynamicCache路由,如果是的话,删除来源路由的name
if (from.meta.dynamicCache) {
if (to.meta.dynamicCache) {
const index = cachedComponents.value.indexOf(from.name)
if (index > -1) {
cachedComponents.value.splice(index, 1)
}
}
}
})
</script>
这个方案的好处是不用Pinia/Vuex,但要注意路由的name必须和组件的name一致,而且如果有多个层级的话,会不会混乱,所以还是推荐用Pinia/Vuex。
高频场景三:清空所有缓存池里的组件
比如用户点击了“退出登录”,这时候所有的缓存都要清掉,下次登录的时候,所有页面都要重新加载。
这个场景的核心思路有两个:一个是直接把include/exclude数组清空;另一个是直接清空Keep-Alive内部的__v_cache属性。
方案3.1:把include/exclude数组清空
这个方案是最稳的,不会用到内部属性,推荐用这个。
如果用了之前的Pinia/Vuex的话,直接调用clearAllCachedComponents()方法就行了:
// 退出登录的逻辑里
import { useCacheStore } from '@/stores/cache'
const cacheStore = useCacheStore()
cacheStore.clearAllCachedComponents()
// 然后跳转到登录页
router.push('/login')
如果没有用Pinia/Vuex的话,直接把App.vue里的cachedComponents数组清空就行了:
// 退出登录的逻辑里,假设cachedComponents是在App.vue里定义的,那怎么获取呢?可以用provide/inject,或者把它放在window对象里?放在window对象里不推荐,因为污染全局,还是用provide/inject吧。
// App.vue里provide
provide('cachedComponents', cachedComponents)
provide('clearAllCachedComponents', () => {
cachedComponents.value = ['Home'] // 假设首页还要缓存
})
// 退出登录的组件里inject
import { inject } from 'vue'
const clearAllCachedComponents = inject('clearAllCachedComponents')
clearAllCachedComponents()
方案3.2:清空Keep-Alive内部的__v_cache属性
这个方案是紧急情况用的,比如你没有用include/exclude数组,或者include/exclude数组清空了,但缓存还是没清掉(这种情况很少见)。
怎么获取Keep-Alive的实例呢?可以用ref给<keep-alive>加一个引用,然后通过keepAliveRef.value.$.vnode.component.subTree.component.exposed?不对,Vue3里<keep-alive>是一个内置组件,没有暴露exposed,所以可以直接访问keepAliveRef.value.$,然后找到__v_cache属性,它是一个Map对象,直接clear()就行了:
<!-- App.vue -->
<template>
<router-view v-slot="{ Component, route }">
<transition name="fade" mode="out-in">
<keep-alive ref="keepAliveRef">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</template>
<script setup>
import { ref, provide } from 'vue'
const keepAliveRef = ref(null)
// 提供清空所有缓存的方法
provide('clearAllKeepAliveCache', () => {
if (keepAliveRef.value) {
// __v_cache是内部属性,可能会在Vue的更新中改变,所以慎用
const cache = keepAliveRef.value.$.__v_cache
if (cache) {
cache.clear()
// 还要清空__v_keys,因为LRU策略用到了这个
const keys = keepAliveRef.value.$.__v_keys
if (keys) {
keys.length = 0
}
}
}
})
</script>
然后在退出登录的组件里inject这个方法,调用就行了。 不过这个方案的坏处是用到了内部属性,Vue官方不推荐使用,因为可能会在Vue的小版本更新中改变,导致代码失效,所以还是推荐用方案3.1。
三种方案怎么选?
现在给你总结一下,方便你根据自己的项目情况选择:
- 如果是从详情页返回列表页,只清除当前这次修改过的列表页:
- 推荐用「路由守卫的meta配合activated钩子」,不用额外的状态管理库,简单方便;
- 如果项目里已经用了Pinia/Vuex,也可以用「Pinia/Vuex全局状态管理信号」,更灵活,适合多个页面控制的情况。
- 如果是从E页面跳转到C/D/E任意一个,清掉前一个:
- 推荐用「Pinia/Vuex全局管理exclude/include数组」,可控性强;
- 如果是同一个组件的不同路由参数,直接加「key=route.fullPath」就行了,不用这么复杂。
- 如果是清空所有缓存池里的组件:
- 推荐用「把include/exclude数组清空」,稳;
- 紧急情况才用「清空Keep-Alive内部的__v_cache属性」,慎用。
最后再提醒你几个注意事项:
- 不管用哪个方案,组件的name属性一定要加,不然Keep-Alive的include/exclude匹配不到;
- 如果用
<script setup>,可以用defineOptions来加name,也可以用两个<script>标签,一个写setup,一个写name; - 如果用了transition和keep-alive一起用,transition要包裹keep-alive,而不是反过来;
- Keep-Alive的max属性可以限制最大缓存数量,超过就自动清最久不用的,这个属性也很有用,可以防止缓存太多组件导致内存泄漏。
好了,今天的内容就讲到这里,希望能帮你解决Vue3 Keep-Alive清不掉的问题,如果还有其他问题,欢迎在评论区留言。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



