[VUE]深入了解keep-alive

479 阅读4分钟

前言

本人在一家中小公司工作了几年,一开始只负责前端,到后来用nodejs写服务以及负责一些团队基础设施等运维工作。由于做的事情太杂,最近想重新系统地整理一下前端的知识,因此写了这个专栏。我会尽量写一些业务相关的小技巧和前端知识中的重点内容,核心思想。


在讲keep alive之前,我们必须先看一下vue的动态组件。

动态组件

我们在写vue的时候,很多时候页面上的同一位置上的组件是需要根据不同业务情况进行改变的。那如果使用基本的语法,可能会出现需要写大量 的v-if。

<component1 v-if="type === 'A'"></component1>
<component2 v-else-if="type === 'B'"></component2>
<component3 v-else-if="type === 'C'"></component3>
<component4 v-else></component4>

这显然不是一种好的处理方法,因此我们诞生了一种可以控制同一位置的组件进行不同渲染的需求。那就是动态组件。他的基本语法是:

// template
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component :is="currentTabComponent"></component>
//scripts
computed: {
   currentTabComponent() {
     if(type === 'A'){
       return 'component1';
     }else if(type === 'B'){
       return 'component2';
     }else if(type === 'C'){
       return 'component3';
     }else{
     	return 'component4';
     }
   }
}

这样我们就可以把组件变化的逻辑放在script中,让我们的代码逻辑性更强,后续组件的增减也不需要再改动template部分了。

同时在上述示例中,is属性的currentTabComponent 可以包括

  • 已注册组件的名字,或
  • 一个组件的选项对象

为什么要有keep alive

我们了解过了动态组件之后,回过头来讲,为什么要有keep-alive?

因为动态组件的出现,确实很好的解决了上述的业务需求。可是又出现了一个问题。我们的动态组件在每次切换的时候,都需要重新选(把生命周期跑一遍)。这样假如我其中几个组件实际上是很常用的,在频繁的切换中。浏览器的渲染成本是很大的,因此我们需要一个方法,让动态组件在渲染了之后,保存在内存里,下次切换的时候,直接显示,不需要重新渲染。这就是keep-alive的作用。

基本语法

keep-alive的写法也很简单

<!-- 失活的组件将会被缓存!-->
<keep-alive>
  <component :is="currentTabComponent"></component>
</keep-alive>

这样被包括的component动态组件渲染过的内容,都会被缓存下来。

说明

<keep-alive>本身是不会渲染DOM的,也不会出现在组件的父组件链中。当组件在 <keep-alive> 内被切换时,它的 mountedunmounted 生命周期钩子不会被调用,取而代之的是 activateddeactivated。(这会运用在 <keep-alive> 的直接子节点及其所有子孙节点。)

props

  • include - string | RegExp | Array。只有名称匹配的组件会被缓存。
  • exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。
  • max - number | string。最多可以缓存多少组件实例。

从源码看原理

那么我们从源码来看看keep-alive是如何实现的。

// keep-alive本身是一个vue组件
export default {
  name: 'keep-alive',
  abstract: true, // 说明这是一个抽象组件,不需要渲染dom

  props: { // 声明props
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number]
  },

  created() {
    // keep-alive组件加载的时候创造了一个对象和一个数组
    // 对象 {key:组件实例}  数组中存储的都事缓存组件对应的key
    // 其实数组中存储的都是 缓存对象中可以存在的属性名
    this.cache = Object.create(null)
    this.keys = []
  },

  destroyed() {
    // keep-alive组件被销毁的时候  所有缓存的组件都要跟着被销毁
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted() {
    //  只要include发生了改变 那么就会执行后边对应的回调函数
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })

    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render() {
    const slot = this.$slots.default // 获取<keep-alive>标签内的内容
    const vnode = getFirstComponentChild(slot)
    const componentOptions = vnode && vnode.componentOptions // 获得组件配置信息
    if (componentOptions) {
      // check pattern
      const name = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        //  判断组件的name 如果不在include或在exclude中就直接返回组件,不作缓存
        return vnode
      }
      // 缓存组件
      const { cache, keys } = this
      // 获取组件的key
      const key = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) {
        // 如果组件在缓存里拿缓存的内容渲染
        vnode.componentInstance = cache[key].componentInstance
        // 更新组件的排序
        remove(keys, key)
        keys.push(key)
      } else {
        // 如果组件不在缓存里,就缓存起来
        cache[key] = vnode
        keys.push(key)
        // 如果缓存已经到最大限制,则删除使用率最小的。
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0]) // 返回组件
  }
}

补充:LRU 算法

在上述keep-alive的源码中,我们看到了组件内部有一个数组存放着这些缓存的组件。这里其实还用到了LRU算法。所谓的LRU算法核心思想其实就是将使用率最高的组件放到数组的尾部。这样在数组另一边的组件,就是使用频率最抵的,在数组长度到达限制之后,优先把他删除。

1.假设现在有长度为5的数组[1,2,3,4]
2.此时有一个新的组件假如,因为数组长度未满,直接插入数组。
	[1,2,3,4].push(5)
	[1,2,3,4,5]
3.然后key为3的组件又被渲染了一次,因为3已经在数组中,我们把3移动到尾部
	remove([1,2,3,4,5], 3)
	keys.push(3)	
	[1,2,4,5,3]
4.如果这时有一个新的组件6加入,因为数组中没有6,而且数组长度已满。需要先把最少用到的组件先删除,再缓存新的。
	[1,2,4,5,3].shift()
	[2,4,5,3].push(6)
	[2,4,5,3,6]

按上述的原理执行,就可以确保数组的尾巴组件是最新,最常用的。而头部的组件是使用率最抵的。