美文网首页
Vue 缓存

Vue 缓存

作者: Wermdany | 来源:发表于2020-06-02 22:54 被阅读0次

    本文是对 vue-element-admin 源码研究,根据项目中缓存方面和 Tagviews 实现,进行改进,同时研究 Vue 内置组件 keep-alive 的用法和存在问题。

    基础

    keep-alive 基础文档API 文档

    其中需要注意以下几点:

    1. keep-alive 本质是把应该销毁的组件缓存起来,当再次需要的时候去读取缓存的组件信息而不是重新渲染,所以 keep-alive 必须包裹一个组件才能生效。

    2. 使用了 include and exclude 会按照这个规则进行匹配缓存那些页面,不使用会缓存所有。

    3. 如果使用了第二条的筛选规则,那么必须配置对照和 name,不然无法正确缓存。

    文档原句:
    匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配

    1. keep-alive 内部的 router-view ,填写 key 的时候,需要谨慎 ,不然会出现问题。

    比如在编辑信息的时候,用户打开了两个标签页使用了同一个组件,不使用 key 就会复用这同一个组件 但是我们需要的是渲染两个,使用不同的 key 就会分别渲染两个,而有时候 key 又会生成多余的页面。

    1. 取消缓存页面只需要把 include and exclude 中不需要缓存的 name 删除即可,因为源代码中会监听这个两个字段,删除缓存的组件。
      mounted () {
        this.$watch('include', val => {
          pruneCache(this, name => matches(val, name))
        })
        this.$watch('exclude', val => {
          pruneCache(this, name => !matches(val, name))
        })
      }
    

    src/core/components/keep-alive 74-81

    vue-element-admin 中的缓存

    默认只实现了一层缓存,对缓存页面进行刷新、删除等操作。

    定一个目标

    1. 实现多层嵌套下,对页面进行缓存,同时可以进行删除、刷新。
    2. 动态路由 可打开多个并同时进行分别缓存。

    开始

    本篇使用 include 对缓存页面进行新增和删除,不考虑默认全部缓存的情况

    嵌套缓存的实现

    本文例子使用了三层路由:App.vueMain.vue (布局) 、其他第三层路由,只有第二层和第三层启动了缓存,称为 第一层缓存和第二层缓存 。

    缓存路由树的实现

    参照了 vue-element-admin 中 tagsViews 的实现在 Vuex 中生成了一个一维数组,实现一层缓存。

    https://github.com/PanJiaChen/vue-element-admin/blob/v4.0.0/src/store/modules/tagsView.js

    想要实现多层嵌套缓存 必须建立多维数组

    经过实验和思考后使用 this.$route.matched 对路由信息进行转化为树形结构

    matched介绍

    const regex = /\/:\w+/g;
    /**
     * 把 matched 格式化为树形格式
     * @param {Array} matched
     * @param {String} name
     */
    function formatMatched(matched, name, parent, path) {
      let route = {
        name: "",
        parent
      };
      matched = matched.slice(1);
      route.name = matched[0].name;
      if (regex.test(matched[0].path)) {
        route.many = true;
      }
      if (matched.length == 1) {
        route.path = path;
      }
      if (matched[0].name !== name) {
        route.children = [].concat(formatMatched(matched, name, route, path));
      }
      return route;
    }
    

    一个节点的数据信息为

    {
    name: "", //组件的name 主要用于 inclues
    path: "", // 区分相同 name 的 页面
    many:boolean,//是否是动态路由
    children:[], //子类
    parent: [] // 父类映射,用于删除和修改,每次修改删除都遍历整个树太消耗性能了
    }
    

    每次切换页面都会生成一个当前路由信息的 单分支树 与总树进行 diff 合并或删除

    新增

    /**
     * 新增一个缓存节点
     */
    function addCached({ cachedViews }, view) {
      let { matched, name, path } = view;
      if (!matched) return;
      const format = formatMatched(matched, name, cachedViews, path);
      mergeCached(cachedViews, format);
    }
    
    /**
     * 合并 cache
     */
    function mergeCached(all, format) {
      let index = all.findIndex(v => v.name === format.name);
      if (index == -1) {
        all.push(format);
      } else {
        if (format.children && format.children.length) {
          mergeCached(all[index].children, format.children[0]);
        } else {
          //如果是动态路由则可以添加多个,在销毁的时候只有全部关闭才会取消缓存
          if (
            format.many &&
            format.path &&
            all.findIndex(v => v.path === format.path) === -1
          ) {
            all.push(format);
          }
        }
      }
    }
    

    删除

    /**
     *
     * @param {*} param
     * @param {*} view
     */
    function removeCached({ cachedViews }, view) {
      let { matched, name, path } = view;
      if (!matched) return;
      const format = formatMatched(matched, name, cachedViews, path);
      delCached(cachedViews, format);
    }
    
    function delCached(all, format) {
      let index = all.findIndex(v => {
        if (v.path && format.path) {
          return v.name === format.name && v.path === format.path;
        } else {
          return v.name === format.name;
        }
      });
      if (index == -1) {
        return;
      } else {
        if (format.children && format.children.length) {
          delCached(all[index].children, format.children[0]);
        } else {
          let parent = all[index].parent;
          all.splice(index, 1);
          if (!all.length && !Array.isArray(parent)) {
            delParentCached(parent);
          }
        }
      }
    }
    

    在使用的时候根据这一棵总数获取想要的树形获取想要的数据,比如第一层节点 name 获取 使用 Vuex 的 Getter

     cachedViews: state => state.tagsView.cachedViews.map(v => v.name),
    

    获取第二层的缓存 name

      findCachedByName: state => name => {
        let children = state.tagsView.cachedViews.find(v => v.name === name);
        if (!children) {
          return [];
        }
        return children.children
          .map(v => v.name)
          .filter((v, i, a) => a.indexOf(v) === i);
      },
    

    获取其他层次的 name 需要另行封装,但是项目中最多也就是实现三层路由,进行两层缓存,所以目前不考虑。

    到此缓存数据树已经实现,但是在页面中的操作还有很多坑,和其解决思路。

    页面中的设置

    在第一层缓存中使用 Vuex 的 Getter 获取 cachedViews

    <keep-alive :include="cachedViews">
        <router-view :key="key" />
    </keep-alive>
    <script>
    import { mapGetters } from "vuex";
    export default {
      name: "AppMain",
      computed: {
        ...mapGetters(["cachedViews"]),
        key() {
          if (this.$route.matched.length > 1) {
            return this.$route.matched[1].path;
          } else {
            return this.$route.path;
          }
        }
      }
    };
    </script>
    

    key 必不可少 , 如果 路由嵌套层次大于等于1 就取 matched 的第二层 path,因为我们当前是第二层路由,第一层是 App.vue , 如果等于第二层就取当前的路由 path

    在第二层缓存中使用 Vuex Getter 的函数形式获取确定的缓存页面 name

    <template>
      <keep-alive :include="include">
        <router-view :key="key" />
      </keep-alive>
    </template>
    <script>
    export default {
      name: "authorityAuth",
      data() {
        return {
          key: this.$route.path
        };
      },
      computed: {
        include() {
          return this.$store.getters.findCachedByName("authorityAuth");
        }
      },
      watch: {
        $route(v) {
          if (v.name.includes("authorityAuth")) {
            this.key = v.path;
          }
          if (this.include.length === 0) {
            this.key === undefined;
          }
        }
      }
    };
    </script>
    
    

    在第二层的缓存的时候,key值处理比较复杂,原本是直接使用this.$route.path,但是出现了非常致命的问题。

    主要原因是:

    Vue 缓存的页面,由于属性劫持的原因,即使被缓存了,$route的变化还会触发变化,$route变化,触发了 key 的变化 从而制造多余无意义的页面如下:

    只有第一个页面时需要的

    组件被缓存后,由于 key 值绑定 $route.path 当页面切换时,key发生改变会创建大量的无用页面占用内存,导致页面迅速卡死。

    所以引出一个问题,缓存的页面是否需要继续活跃属性变化,但是数据劫持是 Vue 的核心,目前没有任何办法能从根源解决,即,短时间冻结劫持。

    目前解决方法是在第三层 <route-view /> 中缓存 key ,只有当前页面切换是当前的缓存的子页面才会改变 key。

    小结

    通过这种方式,可以在一定程度上实现多层缓存和删除,但是如果牵扯到缓存的刷新和动态路由缓存等问题,就会发现 keep-alive 存在的很多缺陷,下面会一一介绍.

    当前思路下其他的嵌套缓存方案(废弃)

    在尝试嵌套缓存的时候,还进行了其他的尝试:

    这种方案本质是 直接在 vue-element-admin 缓存方案中直接套用 嵌套缓存,并非参照系统的本身问题,因为 vue-element-admin 本身需求就是缓存一层。

    这种方案本质还是在于 key 的处理上 ,在上文的基础上进行一点点修改:

    1. 在第一层缓存中 key 值总是取最底层的 path 即 this.$route.path ,试想一下,无论是二层嵌套路由或者是三层嵌套路由,永远都是最底层的 path ,表现结果是:
    造成了更大的性能问题!

    由上图可以看到 造成了更加严重的性能问题!

    有两点困难之处:

    1. 上文说的 缓存页面内部的劫持依然活跃 key 的变化创造了更多的无用页面。
    2. 由于每一个二级缓存都创建了 AuthorityAuth 组件, 也就造成了 无法删除缓存,因为它们的 name 都是 AuthorityAuth ,删除一个就换导致全部缓存删除。

    keep-alive 确定缓存是以 name 为基准的 ,这导致在在一个组件创建不同的 key 达到 复用,比如缓存多个动态路由 ,无法精准的删除某一个页面。

    动态组件缓存问题

    这个问题和上一段写的问题是同一个,由于动态路由,使用的同一个组件,name 是相同的,我们可以通过 key 打开多个页面,但是我们却没办法精准的控制每个页面的缓存和刷新。

    我们只能实现:全部关闭后全部清空。

    遗留的问题还有:一个刷新,则全部刷新

    目前实现是打开多个无法刷新,因为,为了实现全部关闭后取消缓存,也就是说在缓存树中会创建多个 name 相同,但是 path 不同的缓存信息,最后再去重得到 include。

    其他缓存思路

    网上还有很多大佬有很多的想法来实现缓存页面,大致可分:

    1. 默认缓存所有,手动调用 vm.$destroy() 注销组件。

    2. 通过查询 Vnode 找到 keep-alive 的 cache 手动删除缓存。

    3. 不使用 keep-alive 页面切换保存 data 的属性。

    等等等。。。。

    但是我感觉还是使用 keep-alive 比较好,但是 keep-alive 拥有两个缺陷 。

    keep-alive 的局限性

    1. 缓存的页面内部使用的劫持属性还是活跃的,这会导致其他页面的操作影响缓存的页面,比如 key 值绑定问题。

    2. keep-alive 在缓存 动态路由的问题,相同的 name 可以使用 key 创建不同的 实例,但是我们只能用 name 去操作这一系列页面 。

    总结

    如果不考虑以上出现的问题,那么本文还是可以解决,一般遇到的所有缓存问题。

    源码

    参考资料

    Vue Key

    一句话来说就是不同的 key 会被 Vue 当成不同的元素,即使是使用了相同的组件,会被创建多份,这在配合路由和缓存使用时尤其重要。

    Vue 缓存

    相关文章

      网友评论

          本文标题:Vue 缓存

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