Vue 缓存
本文是对 vue-element-admin 源码研究,根据项目中缓存方面和 Tagviews 实现,进行改进,同时研究 Vue 内置组件
keep-alive
的用法和存在问题。
基础
其中需要注意以下几点:
-
keep-alive 本质是把应该销毁的组件缓存起来,当再次需要的时候去读取缓存的组件信息而不是重新渲染,所以 keep-alive 必须包裹一个组件才能生效。
-
使用了
include
andexclude
会按照这个规则进行匹配缓存那些页面,不使用会缓存所有。 -
如果使用了第二条的筛选规则,那么必须配置对照和
name
,不然无法正确缓存。
文档原句:
匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配。
- keep-alive 内部的
router-view
,填写 key 的时候,需要谨慎 ,不然会出现问题。
比如在编辑信息的时候,用户打开了两个标签页使用了同一个组件,不使用 key 就会复用这同一个组件 但是我们需要的是渲染两个,使用不同的 key 就会分别渲染两个,而有时候 key 又会生成多余的页面。
- 取消缓存页面只需要把
include
andexclude
中不需要缓存的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 中的缓存
默认只实现了一层缓存,对缓存页面进行刷新、删除等操作。
定一个目标
- 实现多层嵌套下,对页面进行缓存,同时可以进行删除、刷新。
- 动态路由 可打开多个并同时进行分别缓存。
开始
本篇使用
include
对缓存页面进行新增和删除,不考虑默认全部缓存的情况
嵌套缓存的实现
本文例子使用了三层路由:
App.vue
、Main.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
对路由信息进行转化为树形结构
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 的处理上 ,在上文的基础上进行一点点修改:
- 在第一层缓存中 key 值总是取最底层的 path 即
this.$route.path
,试想一下,无论是二层嵌套路由或者是三层嵌套路由,永远都是最底层的 path ,表现结果是:
由上图可以看到 造成了更加严重的性能问题!
有两点困难之处:
- 上文说的 缓存页面内部的劫持依然活跃 key 的变化创造了更多的无用页面。
- 由于每一个二级缓存都创建了
AuthorityAuth
组件, 也就造成了 无法删除缓存,因为它们的 name 都是AuthorityAuth
,删除一个就换导致全部缓存删除。
keep-alive 确定缓存是以 name 为基准的 ,这导致在在一个组件创建不同的 key 达到 复用,比如缓存多个动态路由 ,无法精准的删除某一个页面。
动态组件缓存问题
这个问题和上一段写的问题是同一个,由于动态路由,使用的同一个组件,name 是相同的,我们可以通过 key 打开多个页面,但是我们却没办法精准的控制每个页面的缓存和刷新。
我们只能实现:全部关闭后全部清空。
遗留的问题还有:一个刷新,则全部刷新
目前实现是打开多个无法刷新,因为,为了实现全部关闭后取消缓存,也就是说在缓存树中会创建多个 name 相同,但是 path 不同的缓存信息,最后再去重得到 include。
其他缓存思路
网上还有很多大佬有很多的想法来实现缓存页面,大致可分:
-
默认缓存所有,手动调用
vm.$destroy()
注销组件。 -
通过查询 Vnode 找到 keep-alive 的 cache 手动删除缓存。
-
不使用 keep-alive 页面切换保存 data 的属性。
等等等。。。。
但是我感觉还是使用 keep-alive 比较好,但是 keep-alive 拥有两个缺陷 。
keep-alive 的局限性
-
缓存的页面内部使用的劫持属性还是活跃的,这会导致其他页面的操作影响缓存的页面,比如 key 值绑定问题。
-
keep-alive 在缓存 动态路由的问题,相同的 name 可以使用 key 创建不同的 实例,但是我们只能用 name 去操作这一系列页面 。
总结
如果不考虑以上出现的问题,那么本文还是可以解决,一般遇到的所有缓存问题。
参考资料
Vue Key
一句话来说就是不同的 key 会被 Vue 当成不同的元素,即使是使用了相同的组件,会被创建多份,这在配合路由和缓存使用时尤其重要。