最近在看一个后台管理类项目时,发现有个刷新的功能,即tab栏中每个tab上有个刷新按钮,点击后并非像F5那样整页刷新,而是只刷新当前路由:
基本原理也很简单,将<router-view>用v-if控制一下,并借助$nextTick进行切换:
// 路由容器组件
// template
<router-view v-if="!refreshing" />
// script
methods: {
reload() {
this.refreshing = false;
this.$nextTick(() => {
this.refreshing = true;
});
}
}
之后将reload用正确的方法传递给执行刷新的组件即可(例如若是路由组件自身控制自身的刷新,用provide/inject传递给就好)。
如果<router-view>本来就没有进行缓存,那么这样也就足够了,但若<router-view>用<keep-alive>进行包裹而进行缓存的话,只这样就不行了,只会使得路由组件在actived/deactivated之间切换,无法销毁/重建路由组件。
既然无法自动销毁路由组件,那自己主动销毁可以么,类型于下面这样:
// 将路由组件自身用作参数传递
reload(vm) {
vm.$destroy();
this.refreshing = false;
this.$nextTick(() => {
this.refreshing = true;
});
}
先试下,好像可以了,被刷新组件的beforeDestroy/created/mounted依次被触发:
但紧接着,问题出现了,如果执行过一次刷新,那本来的缓存功能失效了,即切换到其他路由再切换回来,就会从created阶段开始重新渲染,而并非原本那样只触发deactivated。
实际上,这已经是一个老话题了,github上甚至有个讨论量相当大的issue,其本质也就是<keep-alive>没去处理已经销毁的组件。比较主流,或者说简单一点的方法,就是动态地修改<keep-alive>的include(或exclude),即动态的将被缓存的路由组件的name从include(或exclude)添加/删除。
不过,让我发现这个问题的后台管理类项目并没有这么做,而是实现了一个自己的<keep-alive>来解决这个问题。本着学习源码的目的,来研究一下是怎么做的。
其实,<keep-alive>之所以能发挥缓存的作用,本质在于其维护了一个cache的对象,每个被缓存的组件都有一个根据一定规则生成的key,cache[key]上记录了被缓存组件的缓存内容,如下:
keep-alive的vnode:
// keep-alive部分源码
const { cache, keys } = this
const key = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
?
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') :
vnode.key
if (cache[key]) {
// 有缓存,使用缓存
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
// 无缓存,记录
cache[key] = vnode
keys.push(key)
}
之所以只主动销毁组件会有问题,原因就是主动销毁组件这个操作并没有通知到keep-alive,导致在刷新时,明明组件已被销毁,但keep-alive仍旧在使用缓存。
明白了原因后,修改方法就呼之欲出了:销毁组件时,要同时清除keep-alive中cache中对应的内容。
现在就剩下一个问题,如何得知被缓存的组件在cache中对应的key是什么?
看源码发现,若vnode.key本不存在,则会使用vue本身对组件定义的cid+tag等生成一个key,而若存在vnode.key,直接使用这个key即可。
vue本身对组件定义的cid等并不容易获取,那使用vnode.key就好了。如何设定vnode.key?在<keep-alive>上绑定一个跟路由相关的key即可:
<router-view v-if="refreshing" :key="$route.fullPath" />
按照这个后台管理类项目的方法,其在自定义的<keep-alive>上维护了一个v-model,每次刷新时修改一下这个v-model,同时<keep-live>里也监控这个v-model,只要值发生变化,就执行清除操作:
// 路由容器组件
// template
<x-keep-alive v-model="refreshCache">
<router-view v-if="refreshing" :key="$route.fullPath" />
</x-keep-alive>
// script
methods: {
reload(key) {
this.refreshCache = key; // 还原this.refreshCache在<keep-live>里进行
this.refreshing = false;
this.$nextTick(() => {
this.refreshing = true;
});
}
}
// <x-keep-alive>
// 本身keep-alive里有个pruneCacheEntry函数,执行类似的操作
function pruneCacheEntry2(cache, key, keys) {
const cached = cache[key]
if (cached) {
// cached本就保存了各被缓存组件的vnode,那么组件的销毁也在此处进行 cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
watch: {
value: function(val) {
if (val) {
const {cache, keys} = this
pruneCacheEntry2(cache, val, keys)
this.$emit('input', []) // 还原路由容器组件的v-model
}
}
}
至此,问题就解决了。当然,实际工作中不一定非要像这样修改官方的组件,但本着浅学一下keep-alive的源码,该方法还是相当值得借鉴的。