Vue 缓存

2020-06-02  本文已影响0人  Wermdany

本文是对 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 缓存

上一篇下一篇

猜你喜欢

热点阅读