美文网首页程序员Vue.js前端专刊
vue 组件缓存清除实践

vue 组件缓存清除实践

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


      view.png
    5. 现在点击导航栏 1、2、3、4 之后查看 vue-devtools 可以看到,列表 1、2、3 都被缓存下来了
      unhandler-cache-result.png
    需求描述

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

    初级缓存组件清除实现
    • demo 场景描述之路由配置里面,我在元信息里面添加了一个 level 字段,这个字段是用来描述当前组件的级别,level 越高代表是深层嵌套的组件,从 1 起步;
      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 里面执行该方法来直接销毁当前组件
      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 (obj, key) {
      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 { keys, cache } = parent.componentInstance
        const componentName = componentOptions.Ctor.options.name
        const key = !$vnode.key
                    ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
                    : $vnode.key
        const routeLevel = $route.meta.level
        const { include } = inArray(componentName, includes)
        if (include && !hasOwn(mc_cachedCompnentsInfo, componentName)) {
          mc_cachedCompnentsInfo['cache-com::' + componentName] = {
            // 组件名称
            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(mc_cachedCompnentsInfo, compName)) {
              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(mc_cachedCompnentsInfo, compName)) {
          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 里面执行移除方法方法即可
      remove-cache-method-2.png
      你还可以在组件内的 beforeRouteLeave 钩子函数里面执行移除某些组件的逻辑
      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 该方法来清除任意你想要销毁的组件。
    写在最后

    相关文章

      网友评论

        本文标题:vue 组件缓存清除实践

        本文链接:https://www.haomeiwen.com/subject/xcrcsqtx.html