【Vue】【内置组件】KeepAlive

145 阅读6分钟

KeepAlive的简介

  • 概念

    • Vue的一个内置组件。包裹动态组件,会缓存不活跃的组件实例,而不是销毁它们
    • 是一个抽象组件。它自身不会渲染成一个DOM元素,也不会出现在父组件链中
  • 作用/目的

    KeepAlive内部组件,多个组件间动态切换时,缓存被移除的组件实例。这样KeepAlive内部的组件来回切换时,就不需要重新创建组件实例,而是直接使用缓存中的实例。

    • 一方面可以避免创建组件带来的效率开销;
    • 另一方面可以保留组件的状态
    • 防止重复渲染DOM,减少加载时间及性能消耗,提高用户体验性。
  • 缺点

    当组件里包含大量的内容的时候会占用更多的内存空间,KeepAlive相当于是空间换时间的做法。

KeepAlive的使用方法

  • 属性:
    • include:只有与 include 名称匹配的组件才会被缓存。会根据组件的name选择进行匹配
    • exclude:任何名称与 exclude 匹配的组件都不会被缓存。会根据组件的name选择进行匹配
    • max:最大缓存数。当缓存的实例超过这设置的数时,vue会移除最久没有使用的组件缓存
  • 使用:
    • includeexclude:决定哪些组件可以进入缓存。可以是字符串、正则或者数组。

        <!-- 用逗号分隔的字符串 -->
        <KeepAlive include="a,b">
          <component :is="view"></component>
        </KeepAlive>
      
        <!-- 正则表达式 (使用 `v-bind`) -->
        <KeepAlive :include="/a|b/">
          <component :is="view"></component>
        </KeepAlive>
      
        <!-- 数组 (使用 `v-bind`) -->
        <KeepAlive :include="['a', 'b']">
          <component :is="view"></component>
        </KeepAlive>
      
    • max:

        <KeepAlive :max="10">
          <component :is="view"></component>
        </KeepAlive>
      

KeepAlive的生命周期

KeepAlive中的组件从DOM上移除不是被卸载,而是被缓存依然是组件树的一部分,只是变为不活跃状态。当一个组件实例作为缓存树的一部分插入DOM中时,它将重新被激活。

  • onActivated:

    参考:官方 - KeepAlive - onActivated()

    注册一个回调函数,若组件实例是 KeepAlive 缓存树的一部分,当组件被插入到DOM中调用

    这个钩子在服务器端渲染期间不会被调用

  • onDeactivated:

    参考:官方 - KeepAlive - onDeactivated()

    注册一个回调函数,若组件实例是 KeepAlive 缓存树的一部分,当组件从DOM中被移除时调用。

    这个钩子在服务器端渲染期间不会被调用

  • 注意点

    • onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用。
    • 这两个钩子不仅适用于 缓存的根组件,也适用于缓存树中的后代组件。

KeepAlive原理

在created函数调用时将需要缓存的 VNode 节点保存在 this.cache 中或 render(页面渲染)时,如果VNode的name2符合缓存条件,则会 this.cache 中取出之前缓存的VNode实例渲染。

具体实现

  • 第一步:获取keep-alive包裹着的第一个子组件对象及其组件名;
  • 第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;
  • 第三步:根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键),否则执行第四步;
  • 第四步:在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key);
  • 第五步:最后并且很重要,将该组件实例的keepAlive属性值设置为true。

这里面最重要的就是 LRU(Least recently Used)算法

LRU(Least recently Used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

KeepAlive 的实现正是用到了 LRU策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]

KeepAlive源码分析

  • 源码版本:2.7.14
  • 源码路径:src\core\components\keep-alive.ts

keep-alive的最强大缓存功能是在render函数中实现的

 export default {
  name: 'keep-alive',
  abstract: true, // 抽象组件属性,它在组件实例创建父子关系的时候可以被忽略,发生在 initLifecycle 的过程中

  props: {
    include: patternTypes, // 会被缓存的组件
    exclude: patternTypes, // 不会被缓存的组件
    max: [String, Number] // 可以被缓存的组件的最大数量
  },

  methods: {
    // 缓存组件的方法
    cacheVNode() {
      const { cache, keys, vnodeToCache, keyToCache } = this
      if (vnodeToCache) { // 待缓存组件节点存在
        const { tag, componentInstance, componentOptions } = vnodeToCache
        cache[keyToCache] = {
          name: _getComponentName(componentOptions), // 组件节点名称 - 组件的name字段 || 组件的tag
          tag,
          componentInstance
        }
        keys.push(keyToCache)
        // prune oldest entry
        // 如果配置了max,并且缓存的长度超过了max, 则从缓存中删除第一个
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
        this.vnodeToCache = null
      }
    }
  },

  created() {
    // 初始化对象 cache 和 keys 
    this.cache = Object.create(null) // 使用Object.create()创建的对象除了自身属性之外,原型链上没有任何属性(没有继承Object的任何东西)
    this.keys = []
  },

  destroyed() {
    // 删除所有缓存
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted() {
    // 缓存Vnode
    this.cacheVNode()
    // 检测 include 和 excludede 变化
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  updated() {
    // 缓存Vnode
    this.cacheVNode()
  },

  render() {
    // 获取默认插槽中的第一个组件节点
    const slot = this.$slots.default
    // getFirstComponentChild:获取KeepAlive需要渲染的子组件
    const vnode = getFirstComponentChild(slot)
    // 获取该组件节点的componentOptions
    const componentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      // 获取该组件节点的名称,优先获取组件的name字段,如果name不存在就获取组件的tag
      const name = _getComponentName(componentOptions)
      const { include, exclude } = this
      // 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      // key: 组件的key
      // 获取键,优先获取组件的name字段,否则是组件的tag
      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
      // 在this.cache对象中找到该值,表示该组件有缓存,即命中缓存
      // 命中缓存,直接从缓存取 Vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        // 调整该组件key的顺序,将其从原来的地方删掉,并重新放在最后一个
        remove(keys, key)
        keys.push(key)
      } else { // 没有命中缓存,需要将组件写进缓存中,详情可见方法 - cacheVNode
        // delay setting the cache until update
        this.vnodeToCache = vnode
        this.keyToCache = key
      }

      // @ts-expect-error can vnode.data can be undefined
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

this.cache是一个对象,用来存储需要缓存的组件,存储形式如下:

this.cache = {
    key1: "组件1",
    key2: "组件2",
    // ...
};

获取组件name字段执行的方法 - _getComponentName:

function _getComponentName(opts?: VNodeComponentOptions): string | null {
  return opts && (getComponentName(opts.Ctor.options as any) || opts.tag)
}

getComponentName:src\core\vdom\create-component.ts

export function getComponentName(options: ComponentOptions) {
  return options.name || options.__name || options._componentTag
}

组件销毁时执行方法 - pruneCacheEntry:

function pruneCacheEntry(
  cache: CacheEntryMap,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const entry = cache[key]
  // 当前没有处于被渲染状态的组件,将其销毁
  if (entry && (!current || entry.tag !== current.tag)) {
    // @ts-expect-error can be undefined
    entry.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

mounted中会检测 includeexclude 的变化,当它们发生变化,就表示需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache函数,函数如下:

function pruneCache(
  keepAliveInstance: { cache: CacheEntryMap; keys: string[]; _vnode: VNode },
  filter: Function
) {
  const { cache, keys, _vnode } = keepAliveInstance
  /**
    * 对 this.cache 对象进行遍历,取出每一项的name值,用其与新的缓存规则进行匹配
    * 如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存
    * 则调用pruneCacheEntry函数将其从this.cache对象剔除即可
  */
  for (const key in cache) {
    const entry = cache[key]
    if (entry) {
      const name = entry.name
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

参考: