记一次Keep-Alive与router-view,加key有大坑
背景:
我在做Keep-Alive结合Router-View进行页面级的组件缓存时,缓存的数据在页面切换后没有保留。同时每次路由切换,onMounted和onActivated都会执行。
代码如下:
<router-view v-slot="{ Component }" :key="route.path">
<keep-alive>
<component class="view" :is="Component"/>
</keep-alive>
</router-view>
排查思路:
首先我去官网抄了一段KeepAlive的示例代码,放到了RouterView的外层,发现并无问题。显然是我RouterView的使用方法有误。
官网示例:
<script setup>
import { shallowRef } from 'vue'
import CompA from './CompA.vue'
import CompB from './CompB.vue'
// 进行浅层代理,避免不必要的花销
const current = shallowRef(CompA)
</script>
<template>
<div class="demo">
<!--切换时,current.value值会发生改变 -->
<label><input type="radio" v-model="current" :value="CompA" /> A</label>
<label><input type="radio" v-model="current" :value="CompB" /> B</label>
<KeepAlive>
<component :is="current"></component>
</KeepAlive>
</div>
</template>
于是在经过一系列的排查,最后通过控制变量法,发现是RouterView绑定了Key,导致每次都会重复刷新。这是我的原始写法,把key绑定在了RouterView上。但是我残留的记忆依稀记得,Vue2是可以这样用的。让我对比一下Vue2项目和Vue3项目的Keep-alive+route-view写法。
Vue3:
<router-view :key="route.path" v-slot="{ Component }">
<keep-alive>
<component class="view" :is="Component" />
</keep-alive>
</router-view>
Vue2:
<!-- router-view加上Key可以解决不同路由,但是相同组件,页面不刷新问题 -->
<keep-alive>
<router-view :key="routeKey" />
</keep-alive>
发现大家都是使用Keep-alive,Vue3 会采用Slot的形式,把KeepAlive加在Component的外层,而Vue2会直接把KeepAlive包裹在RouterView的外层。那就很有意思了,那我试一下,在Vue3中把KeepAlive标签也加到RouterView的外层,同时不给router-view加key试试,不出意料,当然是不行的。
<keep-alive>
<router-view v-slot="{Component}" >
<component class="view" :is="Component" />
</router-view>
</keep-alive>
我在几个重要位置中打了断点,在进行路由的切换时发现这种写法在Vue3中会导致获取rawVNode时,rawVNode全部指向了RouterView,这样导致了KeepAlive在缓存组件实例时,并没有缓存到对应的Component,所以这种写法无疑是不可以使用的,官网也不推荐这种写法。
那为什么Vue2可以呢?为此我也在Vue2的项目中打了断点,发现即使,KeepAlive组件中包裹着RouterView,但是在Vue2的this.$slots依旧获取的是业务组件的Vue实例。这里我估计是RouterView的具体实现有关,便不细究下去,有懂行的小伙伴可以说说~
最后让我们解决我最初的问题:
为什么我在RouterView标签上加上了key,就导致KeepAlive不生效呢?
在我的调试下,发现当我加上key时,keepAlive组件在进行路由跳转时,每次都会执行setup函数的初始化流程,即每次都初始化了KeepAlive这个Vue组件,正好印证了“router-view加上Key可以解决不同路由,但是相同组件,页面不刷新问题”。
用这个组件里面的一些核心代码而言就是每次都会重置cache,keys这两个关键内存值,导致KeepAlive失效。
解决方案:
我们可以把key放在component组件上面,这样KeepAlive源码中,Vnode.key就拥有了唯一的值,Cache这个Map也就可以正常进行缓存了。
<router-view v-slot="{ Component }" >
<keep-alive>
<component class="view" :is="Component" :key="route.path" v-if="route.meta?.keepAlive"/>
</keep-alive>
<component class="view" :is="Component" :key="route.path" v-if="!route.meta?.keepAlive"/>
</router-view>
拓展:KeepAlive组件渲染解读
const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,
// Marker for special handling inside the renderer. We are not using a ===
// check directly on KeepAlive in the renderer, because importing it directly
// would prevent it from being tree-shaken.
__isKeepAlive: true,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number],
},
setup(props: KeepAliveProps, { slots }: SetupContext) {
------------------------------------这部分除非刷新,只会在使用KeepAlive组件时执行一次---------------------------------------
const instance = getCurrentInstance()!
const cache: Cache = new Map()
const keys: Keys = new Set()
...
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
--------这部分作为Setup的返回函数,会返回一个VNode实例,这个实例可能读缓存,也可能是rawNode,在KeepAlive组件内部的Component改变时会执行----------
return () => {
const rawVNode = children[0]
...
let vnode = getInnerChild(rawVNode)
const key = vnode.key == null ? comp : vnode.key
const cachedVNode = cache.get(key)
return isSuspense(rawVNode.type) ? rawVNode : vnode
}
},
}