vue 组件缓存清除实践

写在前面
  1. 关于 vue 组件缓存的用法很简单,官网教程 讲解的很详细,关于 vue 组件缓存的带来的弊端网上也有很多探坑的文章,最明显的就是缓存下来的组件如果不做处理,激活的时候就会命中缓存,如果你这个时候希望有新的数据获取,可能你需要在 activated 钩子函数中做一些处理,当然网上有一些做法是通过路由的元信息来做一些处理,如果对组件缓存原理深入了解就知道那些方法可能不能彻底解决问题;
  2. 很繁琐,因为我也做过,所以我不希望在每个缓存组件中都做处理,我更希望的是,我想随意销毁某个缓存组件,我想进行的是向下缓存而不是向上缓存或者都缓存,举个例子,现在有一个列表页,详情页,详情页子页面,我希望,我离开子页面的时候,子页面销毁,离开详情页的时候,详情页销毁;
  3. 现在这些都成为可能了,不是很难理解,但是需要你知道 vue 组件缓存 实现的过程,如果不理解,可以参考 vue 技术揭秘之 keep-alive,因为实现过程是对缓存的逆操作,本文只会介绍组件销毁的实现,不会拓展缓存相关内容。
demo 场景描述
  1. 组件注册
    全局注册四个路由级别非嵌套的组件,包含 nametemplate 选项,部分组件包含 beforeRouteLeave 选项, 分别为 列表 1、2、3、4
    vue 组件缓存清除实践_第1张图片
    components.png
  2. 路由配置
    额外添加的就是路由元信息 meta,里面包含了两个关键字段 levelcompName 前者后面会说,后者是对应的组件名称,即取的是组件的 name 字段
    vue 组件缓存清除实践_第2张图片
    routes.png
  3. 全部配置信息,这里采用的是 vue 混入
    vue 组件缓存清除实践_第3张图片
    mixins.png
  4. 页面结构,顶部固定导航条,可以导航到对应的列表


    vue 组件缓存清除实践_第4张图片
    view.png
  5. 现在点击导航栏 1、2、3、4 之后查看 vue-devtools 可以看到,列表 1、2、3 都被缓存下来了
    vue 组件缓存清除实践_第5张图片
    unhandler-cache-result.png
需求描述

假设上述是一个层层嵌套逻辑,列表1 > 列表2 > 列表3 > 列表4 ,现在需要在返回的时候,依次销毁低层级的组件,所谓低层级指的是相对嵌套较深的,例如列表4相对于列表1、2、3都是低层级。我们先来简单实现这样的一种需求

初级缓存组件清除实现
  • demo 场景描述之路由配置里面,我在元信息里面添加了一个 level 字段,这个字段是用来描述当前组件的级别,level 越高代表是深层嵌套的组件,从 1 起步;
    vue 组件缓存清除实践_第6张图片
    component-level.png
  • 下面是具体去缓存的实现,封装的去缓存方法
// util.js
function inArray(ele, array) {
  let i = array.indexOf(ele)
  let o = {
    include: i !== -1,
    index: i
  }
  return o
}
/**
 * @param {Obejct} to 目标路由
 * @param {Obejct} from 当前路由
 * @param {Function} next next 管道函数
 * @param {VNode} vm 当前组件实例
 * @param {Boolean} manualDelete 是否要手动移除缓存组件,弥补当路由缺少 level 时,清空组件缓存的不足
 */
function destroyComponent (to, from, next, vm, manualDelete = false) {
  // 禁止向上缓存
  if (
      (
        from &&
        from.meta.level &&
        to.meta.level &&
        from.meta.level > to.meta.level
      ) ||
      manualDelete
    ) {
    const { data, parent, componentOptions, key } = vm.$vnode
    if (vm.$vnode && data.keepAlive) {
      if (parent && parent.componentInstance && parent.componentInstance.cache) {
        if (componentOptions) {
          const cacheCompKey = !key ?
                      componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
                      :
                      key
          const cache = parent.componentInstance.cache
          const keys = parent.componentInstance.keys
          const { include, index } = inArray(cacheCompKey, keys)
          // 清除缓存 component'key
          if (include && cache[cacheCompKey]) {
            keys.splice(index, 1)
            delete cache[cacheCompKey]
          }
        }
      }
    }
    // 销毁缓存组件
    vm.$destroy()
  }
  next()
}
// 你可以把它挂载到 vue 原型上
Vue.prototype.$dc = destroyComponent
  • 然后你在全局混入的 beforeRouteLeave 钩子函数里面执行该方法了, 最后一个参数允许你在组件内的 beforeRouteLeave 里面执行该方法来直接销毁当前组件
    vue 组件缓存清除实践_第7张图片
    remove-cache-method-1.png
  • 上述方法通过对比两个组件之间级别(level),符合条件就会从缓存列表(cache, keys)中删除缓存组件,并且会调用 $destroy 方法彻底销毁缓存。
  • 虽然该方法能够实现上面的简单逻辑,也能实现手动控制销毁,但是有一些问题存在:
    1. 手动销毁的时候,只能销毁当前组件,不能销毁指定的某个缓存组件或者某些缓存组件
    2. 只会判断目标组件和当前组件的级别关系,不能判断在两者之间缓存的组件是否要移除,例如,列表1、2、3 均缓存了,如果直接从列表3跳到列表1,那么列表2是没有处理的,还是处于缓存状态的;
    3. 边界情况,即如果目标组件和当前组件以及一样,当前组件也不会销毁,虽然你可以修正为 from.meta.level >= to.meta.level 但是有时候可能需要这样的信息是可配置的
清除缓存的进阶
  • 为了解决上面的问题,下面是一个新的方案:既支持路由级别组件缓存的清除,又支持能定向清除某个或者一组缓存组件,且允许你调整整个项目清除缓存的逻辑;
  • 创建一个包含缓存存储、配置以及清空方法的对象
// util.js
function inArray(ele, array) {
  let i = array.indexOf(ele)
  let o = {
    include: i !== -1,
    index: i
  }
  return o
}

function isArray (array) {
  return Array.isArray(array)
}

const hasOwnProperty = Object.prototype.hasOwnProperty
function hasOwn (key, obj) {
  return hasOwnProperty.call(obj, key)
}
// 创建管理缓存的对象
class manageCachedComponents {

  constructor () {
    this.mc_keepAliveKeys = []
    this.mc_keepAliveCache = {}
    this.mc_cachedParentComponent = {}
    this.mc_cachedCompnentsInfo = {}
    this.mc_removeCacheRule = {
      // 默认为 true,即代表会移除低于目标组件路由级别的所有缓存组件,
      // 否则如果当前组件路由级别低于目标组件路由级别,只会移除当前缓存组件
      removeAllLowLevelCacheComp: true,
      // 边界情况,默认是 true, 如果当前组件和目标组件路由级别一样,是否清除当前缓存组件
      removeSameLevelCacheComp: true
    }
  }

  /**
   * 添加缓存组件到缓存列表
   * @param {Object} Vnode 当前组件实例
   */
  mc_addCacheComponentToCacheList (Vnode) {
    const { mc_cachedCompnentsInfo } = this
    const { $vnode, $route, includes } = Vnode
    const { componentOptions, parent } = $vnode
    const componentName = componentOptions.Ctor.options.name
    const compName = `cache-com::${componentName}`
    const { include } = inArray(componentName, includes)
    if (parent && include && !hasOwn(compName, mc_cachedCompnentsInfo)) {
      const { keys, cache } = parent.componentInstance
      const key = !$vnode.key
                  ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
                  : $vnode.key
      const routeLevel = $route.meta.level
      mc_cachedCompnentsInfo[compName] = {
        // 组件名称
        componentName,
        // 缓存组件的 key
        key,
        // 组件路由级别
        routeLevel
      }
      // 所有缓存组件 key 的列表
      this.mc_keepAliveKeys = keys
      // 所有缓存组件 key-value 集合
      this.mc_keepAliveCache = cache
      // 所有缓存组件的父实例
      this.mc_cachedParentComponent = parent
    }
  }

  // 移除缓存 key
  mc_removeCacheKey (key, keys) {
    const { include, index } = inArray(key, keys)
    if (include) {
      return keys.splice(index, 1)
    }
  }

  /**
   * 从 keep-alive 实例的 cache 移除缓存组件并移除缓存 key
   * @param {String} key 缓存组件的 key
   * @param {String} componentName 要清除的缓存组件名称
   */
  mc_removeCachedComponent (key, componentName) {
    const { mc_keepAliveKeys, mc_cachedParentComponent, mc_cachedCompnentsInfo } = this
    const { componentInstance } = mc_cachedParentComponent
    // 缓存组件 keep-alive 的 cache 和  keys
    const cacheList = componentInstance.cache
    const keysList = componentInstance.keys
    const { include } = inArray(key, keysList)
    if (include && cacheList[key]) {
      this.mc_removeCacheKey(key, keysList)
      this.mc_removeCacheKey(key, mc_keepAliveKeys)
      cacheList[key].componentInstance.$destroy()
      delete cacheList[key]
      delete mc_cachedCompnentsInfo[componentName]
    }
  }

  /**
   * 根据组件名称移除指定的组件
   * @param {String|Array} componentName 要移除的组件名称或者名称列表
   */
  mc_removeCachedByComponentName (componentName) {
    if (!isArray(componentName) && typeof componentName !== 'string') {
      throw new TypeError(`移除的组件可以是 array 或者 string,当前类型为: ${typeof componentName}`)
    }
    const { mc_cachedCompnentsInfo } = this
    if (isArray(componentName)) {
      const unKnowComponents = []
      for (const name of componentName) {
        const compName = `cache-com::${name}`
        if (hasOwn(compName, mc_cachedCompnentsInfo)) {
          const { key } = mc_cachedCompnentsInfo[compName]
          this.mc_removeCachedComponent(key, compName)
        } else {
          unKnowComponents.push(name)
        }
      }
      // 提示存在非缓存组件
      if (unKnowComponents.length) {
        let tips = unKnowComponents.join(` && `)
        console.warn(`${tips} 组件非缓存组件,请在移除缓存列表中删除以上组件名`)
      }
      return
    }

    const compName = `cache-com::${componentName}`
    if (hasOwn(compName, mc_cachedCompnentsInfo)) {
      const { key } = mc_cachedCompnentsInfo[compName]
      this.mc_removeCachedComponent(key, compName)
    } else {
      console.warn(`${componentName} 组件非缓存组件,请添加正确的缓存组件名`)
    }
  }

  /**
   * 移除路由级别的缓存组件
   * @param {Object} toRoute 跳转路由记录
   * @param {Object} Vnode 当前组件实例
   */
  mc_removeCachedByComponentLevel (toRoute, Vnode) {
    const { level, compName } = toRoute.meta
    const { mc_cachedCompnentsInfo, mc_removeCacheRule } = this
    const componentName = Vnode.$vnode.componentOptions.Ctor.options.name
    // exp-1-目标组件非缓存组件,不做处理,但可以根据业务逻辑结合 removeCachedByComponentName 函数来处理
    // exp-2-目标组件是缓存组件,但是未添加 level,会默认你一直缓存,不做处理
    // exp-3-当前组件非缓存组件,目标组件为缓存组件,不做处理,参考 exp-1 的做法
    // 以下逻辑只确保是两个缓存组件之间的跳转
    if (
        level &&
        compName &&
        mc_cachedCompnentsInfo['cache-com::' + compName] &&
        mc_cachedCompnentsInfo['cache-com::' + componentName]
      ) {
      const { removeAllLowLevelCacheComp, removeSameLevelCacheComp } = mc_removeCacheRule
      if (removeAllLowLevelCacheComp) {
        const cachedCompList = []
        // 查找所有不小于当前组件路由级别的缓存组件,即代表要销毁的组件
        for (const cacheItem in mc_cachedCompnentsInfo) {
          const { componentName, routeLevel } = mc_cachedCompnentsInfo[cacheItem]
          if (
              // 排除目标缓存组件,不希望目标组件也被删除
              // 虽然会在 activated 钩子函数里面重新添加到缓存列表
              componentName !== compName &&
              Number(routeLevel) >= level &&
              // 边界处理
              removeSameLevelCacheComp
            ) {
              cachedCompList.push(mc_cachedCompnentsInfo[cacheItem])
          }
        }

        if (cachedCompList.length) {
          cachedCompList.forEach(cacheItem => {
            const { key, componentName } = cacheItem
            const compName = 'cache-com::' + componentName
            this.mc_removeCachedComponent(key, compName)
          })
        }
        return
      }
      // 只移除当前缓存组件
      const { routeLevel } = mc_cachedCompnentsInfo['cache-com::' + componentName]
      if (Number(routeLevel) >= level && removeSameLevelCacheComp) {
        this.mc_removeCachedByComponentName(componentName)
      }
    }
  }
}
// 你可以把它挂载到 vue 原型上
Vue.prototype.$mc = new manageCachedComponents()
  • 使用起来非常简单,只需要你在全局的 activated 函数里面执行添加缓存方法,在全局 beforeRouteLeave 里面执行移除方法方法即可
    vue 组件缓存清除实践_第8张图片
    remove-cache-method-2.png

    你还可以在组件内的 beforeRouteLeave 钩子函数里面执行移除某些组件的逻辑
    vue 组件缓存清除实践_第9张图片
    remove-custom-cache.png
  • 使用上述方法需要注意的事项是
    1. 给缓存组件添加组件名称;
    2. 需要在路由记录里面配置好 compName 选项,并且组织好你的 level,因为在实际业务比 demo 复杂很多;
    3. 缓存组件会激活 activated 钩子,你需要在该函数里面执行添加缓存的方法,不然整个清缓存是不起作用的;
    4. 默认的清除规则是移除所有低层级的缓存组件(即缓存组件列表1、2、3,从列表3跳到列表1,列表2、3均会清除);
    5. 边界情况的也会清除(即如果列表2、3 的 level 相同,从列表3跳到列表2,会清除列表3的缓存);
  • 你可能注意到了一个问题,在整个项目中配置不支持动态修改的,即在整个项目中缓存移除的规则是不同时支持两种模式的,不想麻烦做是因为 vue 混入的缘故,全局的 beforeRouteLeave 会在组件内 beforeRouteLeave 之前执行,所以你懂得...不过你无需担心有死角的清除问题,因为你可以通过 mc_removeCachedByComponentName 该方法来清除任意你想要销毁的组件。
2019/05/04 - 新增对 TS 支持
  • 如果你是 vue + ts 的开发方式,可以采用下面的方式,由于当前 vue (或者 <2.6.10)的版本对 ts 支持不是很好,所以大部分是采用 vue-shims.d.ts 的方式来进行模块拓展,更多的使用细节可参考 vue 官网对 Typescript 的支持 以及 Typescript 模块拓展
  • 下面是文件相对位置关系


    vue 组件缓存清除实践_第10张图片
    file.png
  • vue-shims.d.ts 文件内容
/*
 * @description: 模块拓展类型定义文件
 */
import Vue, { VNode } from 'vue'
import { Route } from 'vue-router'
import ManageCachedComponents from './clear-cache'

export type ElementType = string | number

export interface KeepAliveCachedComponent {
  [key: string]: VNode
}

interface CtorOptions {
  name: string
  [key: string]: any
}

declare module 'vue/types/vue' {
  interface Vue {
    $route: Route
    $mc: ManageCachedComponents
    includes: string[]
    keys?: ElementType[]
    cache?: KeepAliveCachedComponent
  }
  interface VueConstructor {
    cid: number
    options: CtorOptions
  }
}
  • cache-clear.ts 文件
/*
 * @description: TS 版本的缓存移除
 */

import Vue, { VNode } from 'vue'
import { Route } from 'vue-router'
import { ElementType } from './vue-shim'

interface CachedComponentList {
  componentName: string,
  key: string,
  routeLevel: number
}

interface RemoveCachedRules {
  removeAllLowLevelCacheComp: boolean
  removeSameLevelCacheComp: boolean
}

const hasOwnProperty = Object.prototype.hasOwnProperty

const inArray = (ele: ElementType, array: ElementType[]) => {
  const i = array.indexOf(ele)
  const o = {
    include: i !== -1,
    index: i
  }
  return o
}

const isArray = (array: any) => {
  return Array.isArray(array)
}

const hasOwn = (key: ElementType, obj: object) => {
  return hasOwnProperty.call(obj, key)
}

export default class ManageCachedComponents {
  private mc_keepAliveKeys: ElementType[] = []
  private mc_cachedParentComponent: VNode = {}
  private mc_cachedComponentsInfo: CachedComponentList = {}
  public mc_removeCacheRule: RemoveCachedRules = {
    removeAllLowLevelCacheComp: true,
    removeSameLevelCacheComp: true
  }

  /**
   * 从缓存列表中移除 key
   */
  private mc_removeCacheKey (key: ElementType, keys: ElementType[]) {
    const { include, index } = inArray(key, keys)
    include && keys.splice(index, 1)
  }

  /**
   * 从 keep-alive 实例的 cache 移除缓存组件并移除缓存 key
   * @param key 缓存组件的 key
   * @param componentName 要清除的缓存组件名称
   */
  private mc_removeCachedComponent (key: string, componentName: string) {
    const { mc_keepAliveKeys, mc_cachedParentComponent, mc_cachedComponentsInfo } = this
    const { componentInstance } = mc_cachedParentComponent
    const cacheList = componentInstance.cache
    const keysList = componentInstance.keys
    const { include } = inArray(key, keysList)
    if (include && cacheList[key]) {
      this.mc_removeCacheKey(key, keysList)
      this.mc_removeCacheKey(key, mc_keepAliveKeys)
      cacheList[key].componentInstance.$destroy()
      delete cacheList[key]
      delete mc_cachedComponentsInfo[componentName]
    }
  }

  /**
   * 添加缓存组件到缓存列表
   * @param Vue 当前组件实例
   */
  mc_addCacheComponentToCacheList (Vue: Vue) {
    const { mc_cachedComponentsInfo } = this
    const { $vnode, $route, includes } = Vue
    const { componentOptions, parent } = $vnode
    const componentName = componentOptions.Ctor.options.name
    const compName = `cache-com::${componentName}`
    const { include } = inArray(componentName, includes)
    if (parent && include && !hasOwn(compName, mc_cachedComponentsInfo)) {
      const { keys, cache } = parent.componentInstance
      const key = !$vnode.key
                  ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
                  : $vnode.key
      const routeLevel = $route.meta.level
      mc_cachedComponentsInfo[compName] = {
        componentName,
        key,
        routeLevel
      }
      this.mc_keepAliveKeys = keys
      this.mc_cachedParentComponent = parent
    }
  }

  /**
   * 根据组件名称移除指定的组件
   * @param componentName 要移除的组件名称或者名称列表
   */
  mc_removeCachedByComponentName (componentName: string | string[]) {
    if (!isArray(componentName) && typeof componentName !== 'string') {
      throw new TypeError(`移除的组件可以是 array 或者 string,当前类型为: ${typeof componentName}`)
    }
    const { mc_cachedComponentsInfo } = this
    if (isArray(componentName)) {
      const unKnowComponents = []
      for (const name of componentName) {
        const compName = `cache-com::${name}`
        if (hasOwn(compName, mc_cachedComponentsInfo)) {
          const { key } = mc_cachedComponentsInfo[compName]
          this.mc_removeCachedComponent(key, compName)
        } else {
          unKnowComponents.push(name)
        }
      }
      // 提示存在非缓存组件
      if (unKnowComponents.length) {
        let tips = unKnowComponents.join(` && `)
        console.warn(`${tips} 组件非缓存组件,请在移除缓存列表中删除以上组件名`)
      }
      return
    }

    const compName = `cache-com::${componentName}`
    if (hasOwn(compName, mc_cachedComponentsInfo)) {
      const { key } = mc_cachedComponentsInfo[compName]
      this.mc_removeCachedComponent(key, compName)
    } else {
      console.warn(`${componentName} 组件非缓存组件,请添加正确的缓存组件名`)
    }
  }

  /**
   * 移除路由级别的缓存组件
   * @param toRoute 跳转路由记录
   * @param Vue 当前组件实例
   */
  mc_removeCachedByComponentLevel (toRoute: Route, Vue: Vue) {
    const { level, compName } = toRoute.meta
    const { mc_cachedComponentsInfo, mc_removeCacheRule } = this
    const componentName = Vue.$vnode.componentOptions.Ctor.options.name
    if (
        level &&
        compName &&
        mc_cachedComponentsInfo['cache-com::' + compName] &&
        mc_cachedComponentsInfo['cache-com::' + componentName]
      ) {
      const { removeAllLowLevelCacheComp, removeSameLevelCacheComp } = mc_removeCacheRule
      if (removeAllLowLevelCacheComp) {
        const cachedCompList = []
        for (const cacheItem in mc_cachedComponentsInfo) {
          const { componentName, routeLevel } = mc_cachedComponentsInfo[cacheItem]
          if (
              componentName !== compName &&
              Number(routeLevel) >= level &&
              removeSameLevelCacheComp
            ) {
              cachedCompList.push(mc_cachedComponentsInfo[cacheItem])
          }
        }

        if (cachedCompList.length) {
          cachedCompList.forEach(cacheItem => {
            const { key, componentName } = cacheItem
            const compName = 'cache-com::' + componentName
            this.mc_removeCachedComponent(key, compName)
          })
        }
        return
      }
      const { routeLevel } = mc_cachedComponentsInfo['cache-com::' + componentName]
      if (Number(routeLevel) >= level && removeSameLevelCacheComp) {
        this.mc_removeCachedByComponentName(componentName)
      }
    }
  }
}
  • 如果 vue3.0 出来以后,就不需要 vue-shims.d.ts 文件了,到时候使用 ts 会更加方便,当然更希望尤大能够增加缓存操作的 api,这样就不再为了缓存而造各种轮子了。
写在最后
  • 这篇文章主要参考的是 vue组件缓存源码,感兴趣的可以看一下;
  • 本文为原创文章,如果需要转载,请注明出处,方便溯源,如有错误地方,可以在下方留言,欢迎斧正,demo 已经上传到 关于vue缓存清除的个人git仓库

你可能感兴趣的:(vue 组件缓存清除实践)