vue2之keep-alive详解

6,138 阅读7分钟

vue2之keep-alive详解

起因

我有个朋友,在维护公司的项目时候,出现了一个bug,就是使用keepalive的页面居然有时候没有缓存效果。真是奇了个大怪,下面是问题代码。于是本着助人为乐的好品质,我就帮他看了一下,发现事情不简单 image.png

keep-alive是什么

首先在开始前,我们要先了解一下keep-alive这个组件的基本用法。 我这边就长话短说。这个组件的功能只有一个那就是缓存组件,可以让其包裹的组件不销毁,起到一个缓存的作用。

接受可选的三个参数

  • max 缓存的最大组件数量,支持数字和字符串
  • include 需要缓存的数据,支持字符串,数组,正则
  • exclude 不需要缓存的数据,支持字符串,数组,正则 具体的详细介绍可以看这里 keep-alive官方介绍

寻找bug

了解了基本用法,那就要去定位问题了

会不会是include的值匹配的范围太小,导致无法缓存?

会不会是include的值让其无法缓存呢?于是我让我的那个朋友打印了一下include中的值,好家伙返回的是组件name属性组成的数组,那应该不是这个问题了,因为需要缓存的组件在这个数组里

会不会exclude的值匹配的范围太大,导致无法缓存?

不是include造成的那应该就是exclude这个数据太大了,导致其无法缓存。于是我又让我那个朋友打印了一下exclude。结果出人意料,居然是个空数组。我的天啊。这是啥原因?

我看了一下组件可以接受max这个参数,是不是组件内部默认设置了最大缓存数,导致缓存失败?

keep-alive内部是否设置了默认max?

对于这个疑问,本着解决问题的态度,于是我把vue源码拉取下来,准备一探究竟

源码路径src =>core => components => keep-alive.ts github:github.com/vuejs/vue/b… 我这边看的是2.7版本的,好像之前的flow语法全给用ts改写了,不得不说vue团队还是挺强大的

怀着好奇心情看源码

max是不是设置了默认值

于是我打开了源码keep-alive.ts,在当前文件搜了一下max。有两处引用,一次是props相关,一次是使用相关

  1. 这是定义props的地方,可以看出来,这里没有做默认值处理,只是定义了接受传入的props

image.png

  1. 继续看使用的地方,好像也没有默认值处理啊,只是判断max是否定义,和超出他才调用pruneCacheEntry这个方法

image.png

pruneCacheEntry做了什么

那么pruneCacheEntry是什么,他做了什么呢,从变量名,可以大概猜出,这个去除缓存的函数

我这边把函数改成js,方便大家阅读

image.png

看得出,他接受了四个参数,分别是cache,key,keys,current

current看起来是当前keep-alive组件的虚拟dom,那cachekeys是什么呢,从哪里定义的呢

cache,keys是什么呢

于是我找到了这两个变量的声明地方。原来在created这个生命周期中声明了

  • cache 是一个空对象
  • keys 是一个空数组 从字面意思可以看出,这两个是缓存用的,具体他是咋用的?

image.png

初始化

原来在组件初始化的时候,通过mounted这个生命周期调用了cacheVNode这个方法,这个方法拥有缓存的奇效

image.png 具体实现如下

image.png 好家伙这里实现和之前调用max的地方居然是同一个函数

  • 取出组件中的vnodeToCachekeyToCache
  • vnodeToCache中获取的组件名tag,组件实例和选项
  • 将组件存放在缓存中
  • 并把匹配的key值推入keys尾部
  • 清空当前的vnodeToCache 原来如此,那么vnodeToCachekeyToCache从哪里定义的呢

vnodeToCache和keyToCache

我在render中找到了定义,为了解读方便,我把ts改成了js,并添加了点注释

  render() {
    const slot = this.$slots.default;
    const vNode = getFirstChildren(slot);
    const componentsOptions = vNode.componentOptions;
    // 获取第一个有效组件的配置项
    if (componentsOptions) {
      const { exclude, include } = this;
      const name = getComponentName(componentsOptions);
      //如果传递了限制 这里如果没有命中规则,直接返回
      if ((include && (!name || !matches(include, name))) || (exclude && name && matches(exclude, name))) {
        console.log('不符合缓存条件,直接返回');
        return vNode;
      }
      console.log(componentsOptions);
      const { cache, keys } = this;
      // 这里获取组件的key,如果用户没有传入key,则使用组件的cid+组件的名称作为key
      const key =
        vNode.key == null
          ? componentsOptions.Ctor.cid + (componentsOptions.tag ? `::${componentsOptions.tag}` : '')
          : vNode.key;
      if (cache[key]) {
        console.log('缓存命中');
        // 将命中的缓存组件拿出来覆盖当前vNode的componentInstance
        console.log(cache[key]);
        vNode.componentInstance = cache[key].componentInstance;
        // 由于需要将当前组件变成最新被使用
        // 1.先删除命中的key
        remove(keys, key);
        // 2.重新追加到尾部,这样可以保证这个组件是最近被使用
        keys.push(key);
      } else {
        console.log('未命中缓存,将其加入缓存', key);
        this.vNodeToCache = vNode;
        this.keyToCache = key;
      }
      vNode.data.keepAlive = true;
    }

    return vNode;
  }

原来是这样,大致流程如下

  1. 首先通过slot获取到了当前默认插槽里的组件
  2. 通过getFirstChildren获取到了正在渲染的第一个组件,这也解释了为什么我在keepalive里写了好多组件,页面渲染的永远是第一个组件
  3. 拿到了第一个正在渲染的组件,取出他的组件名name,先判断用户是否传递了includeexclude,如果匹配上了,且不需要被缓存,就直接返回这个组件
  4. 如果可以被缓存,那么通过判断他的key是否存在,来生成一个唯一key
  5. 通过这个key来从缓存cache查找是否被缓存过
  6. 如果没有缓存过,把key和当前组件分别赋值给vNodeToCachekeyToCache
  7. 如果缓存过,那么将缓存中的这个key对应的组件信息赋值给当前渲染的组件,并且返回

问题解决

等等,我好像找到答案了,通过key去缓存的组件,在看问题代码 image.png

我那个朋友在router-view中传递了key,于是我让他打印了一下,发现每次路由切换,都会生成一个不同的viewkey。我好像找到答案了,我让他去除了key,发现可以缓存了。ok了,问题解决了!

如何更新数据

问题是解决了,可是cacheVNode只在mounted的时候调用,那他如何缓存多个呢?好奇的我发现,在updated中也调用了。

image.png 好了,我悟了。原来如此,在每次render后,都会调用这个函数,从而实现缓存的功效

include和exclude的观察

问题又来了,如果include和exclude变化了,如何进行缓存呢?

于是我在mounted中看到了定义,对这两个参数进行watch监听,每次变换,重新执行pruneCache这个函数

image.png

pruneCache做了什么

我又把ts改成js并且加了点注释,这个函数的功能就只有一个,通过传入的参数,去匹配组件名字是否满足缓存条件,如果不满足,那就去除这个缓存

const pruneCache = (keepAliveInstance, filter) => {
  // 从keepalive组件中获取缓存的组件数据和keys列表
  // 循环获取缓存的key 进行卸载
  const { cache, keys, _vnode } = keepAliveInstance;
  for (let key in cache) {
    if (cache[key]) {
      const name = cache[key].name;
      // 使用name去匹配,不满足这个条件的就卸载
      if (name && !filter(name)) {
        console.log('卸载===>', name);
        pruneCacheEntry(cache, key, keys, _vnode);
      }
    }
  }
};

如何进行最大缓存约束

在这里,我又有个问题,如果设置了最大缓存数,内部如何判断哪些是需要缓存的,哪些是需要删除的?

其实这个问题在函数pruneCacheEntry中有了答案,我这边简单加了点注释。该函数会清除缓存

const pruneCacheEntry = (cache, key, keys, current) => {
  const entry = cache[key];
  if (entry && (!current || current.tag !== entry.tag)) {
    entry.componentInstance.$destroy();
  }
  // cache中的匹配的数据变为null
  cache[key] = null;
  // 删除数组中的数据
  remove(keys, key);
};

这边还得依靠文章开头摘取的代码,默认认为第一项为过期,将它从缓存中去除

if (max && keys.length > parseInt(max)) {
// 卸载过期的缓存,这边默认是数组的第一项
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}

如何保证经常使用的组件不会从缓存中去除

看到这里我总感觉怪怪的,如果我的组件第一个缓存,岂不是第一个从缓存中去除

答案不是的

if (cache[key]) {
// 缓存命中
// 将命中的缓存组件拿出来覆盖当前vNode的componentInstance
console.log(cache[key]);
vNode.componentInstance = cache[key].componentInstance;
// 由于需要将当前组件变成最新被使用
// 1.先删除命中的key
remove(keys, key);
  // 2.重新追加到尾部,这样可以保证这个组件是最近被使用
keys.push(key);
}

这样可以保证最近使用的一次组件在末尾了。好家伙那些写框架的人都这么牛逼的吗

结语

通过本次问题,解决了朋友的问题,也让我对vue中keep-alive组件的实现有了更多的了解。感谢大家看到这里。如果觉得不错可以给小弟点个赞😀😀。如果有理解错误的地方,可以提出,我改!