关于清除keep-alive的缓存

539 阅读3分钟

最近在看一个后台管理类项目时,发现有个刷新的功能,即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的源码,该方法还是相当值得借鉴的。