vue组件keep-alive源码的一些理解

78 阅读4分钟

keep-alive是什么

keep-alive是一个vue内置组件,它有些特殊,它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。被keep-alive包裹的组件实例,会被缓存起来。当组件在 <keep-alive> 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。

使用场景

举个例子,某个电商平台首页,会加载很多图片或视频类的资源,当用户从首页点击某个链接跳转到详情页面的时候,再回来,如果此时刷新页面会比较慢,从而影响用户体验。期望在每次从商品详情页返回至首页时,不会每次都刷新。keep-alive就是用来解决这种场景。简而言之:keep-alive能做到提升系统性能,保存组件的渲染状态。

应用

通常,我们会在keep-alive中缓存动态组件或者vue-router组件。 示例:

<keep-alive :include="[compA, compB]" :exclude="compC" :max="3">
    <component :is="currentComponent"></component>
</keep-alive>
<keep-alive :include="[compA, compB]" :exclude="[compC]" :max='3'>

    <router-view></router-view>

</keep-alive>

源码

name: 'keep-alive',
 abstract: true,
 
 props: {
  include: patternTypes, 
  exclude: patternTypes, 
  max: [String, Number] // 缓存的组件实例数量上限
 },
 
 created () {
  this.cache = Object.create(null) // 缓存对象
  this.keys = [] // 缓存的虚拟dom的键集合
 },
 
 destroyed () {
  for (const key in this.cache) { // 删除所有的缓存
   pruneCacheEntry(this.cache, key, this.keys)
  }
 },
 
 mounted () {
  this.$watch('include', val => {
   pruneCache(this, name => matches(val, name))
  })
  this.$watch('exclude', val => {
   pruneCache(this, name => !matches(val, name))
  })
 },

可以看到,在mounted钩子中,组件会对include和exclude进行实时监听,此处的match函数用来判断当前组件名称是否符合新的include的规则,即当前组件是否会被缓存起来。而pruneCache函数则会遍历当前的缓存对象,判断在新的缓存规则下,哪些组件需要被清理掉。代码读到此处的时候,我产生了一个疑问,“那符合当前缓存规则,但未被缓存起来的组件是怎么处理的呢?” 别急,继续往下看。

render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key: ?string = 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)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

可以看到,在render函数中,假如当前组件不符合缓存规则,则会被直接返回,被渲染为真实dom。 假如当前组件符合缓存规则时,存在两种情况:

其一:已经被缓存过了。此时,keep-alive组件会从缓存对象中取出已被缓存组件,并且移除缓存对象的键值集中原本的键,然后将这个键放到键值集的最后。(这个键值集有什么作用后面会讲)

其二:未被缓存过。此时,keep-alive组件会将此组件添加至缓存对象,并且将此组件的键放到键值集的最后。注意:如果这时候缓存对象的键值集的长度超过缓存的组件实例数量上限max,keep-alive组件会采取措施,将缓存对象中的首个缓存对象移除,保证当前组件能被正常缓存。

最后会将当前组件对象的keepAlive值置为true,它的作用是,在初始化组件钩子函数中,当keepAlive和vnode的组件实例同时为truly时,不再进入$$mount过程,所以在$mount之前的所有钩子函数(beforeCreate 、 created 、 mounted)将不再执行。

针对缓存对象的键值集,这里需要引入一个概念:即LRU置换策略。

LRU置换策略

LRU(Least Recently Used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

钩子

activated

  • 类型Function

  • 详细

    被 keep-alive 缓存的组件激活时调用。

    该钩子在服务器端渲染期间不被调用。

    官网描述如上所示,当切换到被缓存的组件时,会调用activated钩子函数,并且,如果这个组件已经被缓存过,那么这个组件的created、mounted等钩子函数不会执行,但是依然会执行activated。所以,可以在这个钩子函数中更新被缓存页面中的部分或所有数据。比较灵活。

deactivated

  • 类型Function

  • 详细

    被 keep-alive 缓存的组件失活时调用。

    该钩子在服务器端渲染期间不被调用。

此钩子在从被缓存组件切换到其它组件时被调用。类似于非keep-alive组件包裹下的组件中的destoryed钩子。