>vue使用页签模式,组件使用keep-alive缓存,发现页签关闭后缓存组件未销毁,只是出于非活动状态
解决方案
* 复制一份keep-alive源码并修改,关闭页签时传入需要删除缓存的tag,与cache对比,删除cache[tag]
![![](https://img.kancloud.cn/fb/76/fb76b01af1567d89c930b867068bd4b5_1106x1174.png)](images/screenshot_1631065294148.png)
```
import { isDef, isRegExp, remove } from "@/utils/util";
const patternTypes = [String, RegExp, Array];
/* 检测name是否匹配 */
function matches(pattern, name) {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1;
} else if (typeof pattern === "string") {
/* 字符串情况,如a,b,c */
return pattern.split(",").indexOf(name) > -1;
} else if (isRegExp(pattern)) {
return pattern.test(name);
}
/* istanbul ignore next */
return false;
}
/* 优先获取组件的name字段,如果name不存在则获取组件的tag */
function getComponentName(opts) {
return opts && (opts.Ctor.options.name || opts.tag);
}
function getFirstComponentChild(children) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i];
if (isDef(c) && (isDef(c.componentOptions) || c.isAsyncPlaceholder)) {
return c;
}
}
}
}
// 如果include 或exclude 发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache函数,函数如下:
// 在该函数内对this.cache对象进行遍历,取出每一项的name值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry函数将其从this.cache对象剔除即可。
function pruneCache(keepAliveInstance, filter) {
const { cache, keys, _vnode } = keepAliveInstance;
for (const key in cache) {
const cachedNode = cache[key];
if (cachedNode) {
const name = getComponentName(cachedNode.componentOptions);
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode);
}
}
}
}
function pruneCacheEntry(cache, key, keys, current) {
const cached = cache[key];
/* 判断当前没有处于被渲染状态的组件,将其销毁*/
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy();
}
cache[key] = null;
remove(keys, key);
}
export default {
name: "AKeepAlive",
abstract: true,
model: {
prop: "clearCaches",
event: "clear"
},
// 在props选项内接收传进来的三个属性:include、exclude和max。如下:
// include 表示只有匹配到的组件会被缓存,而 exclude 表示任何匹配到的组件都不会被缓存, max表示缓存组件的数量,因为我们是缓存的 vnode 对象,它也会持有 DOM,当我们缓存的组件很多的时候,会比较占用内存,所以该配置允许我们指定缓存组件的数量。
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number],
clearCaches: Array
},
watch: {
clearCaches: function (val) {
if (val && val.length > 0) {
const { cache, keys } = this;
val.forEach(key => {
pruneCacheEntry(cache, key, keys, this._vnode);
});
this.$emit("clear", []);
}
}
},
// 在 created 钩子函数里定义并初始化了两个属性: this.cache 和 this.keys。
// this.cache是一个对象,用来存储需要缓存的组件,它将以如下形式存储:
// this.cache = {
// 'key1':'组件1',
// 'key2':'组件2',
// // ...
// }
// this.keys是一个数组,用来存储每个需要缓存的组件的key,即对应this.cache对象中的键值。
created() {
this.cache = Object.create(null);
this.keys = [this.cache];
},
// 当<keep-alive>组件被销毁时,此时会调用destroyed钩子函数,在该钩子函数里会遍历this.cache对象,然后将那些被缓存的并且当前没有处于被渲染状态的组件都销毁掉。如下:
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys);
}
},
// 在mounted钩子函数中观测 include 和 exclude 的变化,如下:
// 如果include 或exclude 发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache函数,函数如下:
mounted() {
this.$watch("include", val => {
pruneCache(this, name => matches(val, name));
});
this.$watch("exclude", val => {
pruneCache(this, name => !matches(val, name));
});
},
render() {
/* 获取默认插槽中的第一个组件节点 */
const slot = this.$slots.default;
const vnode = getFirstComponentChild(slot);
/* 获取该组件节点的componentOptions */
const componentOptions = vnode && vnode.componentOptions;
if (componentOptions) {
/* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
const name = getComponentName(componentOptions);
const { include, exclude } = this;
if (
/* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode;
}
const { cache, keys } = this;
/* 获取组件的key */
const key =
vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : "")
: vnode.key;
/* 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 */
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
/* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一个 */
remove(keys, key);
keys.push(key);
} else {
/* 如果没有命中缓存,则将其设置进缓存 */
cache[key] = vnode;
keys.push(key);
/* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
/* 最后设置keepAlive标记位 */
vnode.data.keepAlive = true;
}
return vnode || (slot && slot[0]);
}
};
```
新建`src/utils/util.js`
```
export function isDef (v){
return v !== undefined && v !== null
}
/**
* Remove an item from an array.
*/
export function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
export function isRegExp (v) {
return _toString.call(v) === '[object RegExp]'
}
const _toString = Object.prototype.toString
```
修改`TabsView`
```
<template>
<section>
<a-tabs v-model="activePage" type="editable-card" :hide-add="true" @edit="editPage" @change="changePage">
<!-- edit 新增和删除页签的回调,在 type="editable-card" 时有效 -->
<a-tab-pane v-for="page in pageList" :key="page.fullPath">
<span slot="tab" :pagekey="page.fullPath">{{ page.meta.title }}</span>
</a-tab-pane>
</a-tabs>
<div class="tabs-view-content">
<a-keep-alive v-model="clearCaches">
<router-view :key="$route.fullPath" ref="tabContent" />
</a-keep-alive>
</div>
</section>
</template>
<script>
import AKeepAlive from "@/components/cache/AKeepAlive";
export default {
components: { AKeepAlive },
data() {
return {
cachedKeys: [],
clearCaches: [],
keepAliveList: [],
activePage: "",
pageList: [] // 页签的路由数组
};
},
created() {
const route = this.$route;
this.pageList.push(route);
this.activePage = route.fullPath;
// 自定义监听关闭事件
window.addEventListener("page:close", this.closePageListener);
},
mounted() {
this.cachedKeys.push(this.$refs.tabContent.$vnode.key);
},
beforeDestroy() {
window.removeEventListener("page:close", this.closePageListener);
},
watch: {
$route: function (newRoute) {
// 当前路由高亮
this.activePage = newRoute.fullPath;
// 当前路由在页签中不存在时添加新页签
if (this.pageList.findIndex(item => item.fullPath == newRoute.fullPath) == -1) {
this.$nextTick(() => {
this.cachedKeys.push(this.$refs.tabContent.$vnode.key);
});
this.pageList.push(newRoute);
}
}
},
methods: {
changePage(key) {
this.activePage = key;
this.$router.push(key);
},
editPage(key, action) {
this[action](key); // remove
},
remove(key, next) {
// 页签只有一个时 不能关闭
if (this.pageList.length === 1) {
return this.$message.warning("这是最后一页,不能再关闭了");
}
// 当前页签的索引
let index = this.pageList.findIndex(item => item.fullPath === key);
// 删除当前页签
this.pageList.splice(index, 1);
// 清除缓存
this.clearCaches = this.cachedKeys.splice(index, 1);
if (next) {
this.$router.push(next);
} else if (key === this.activePage) {
index = index >= this.pageList.length ? this.pageList.length - 1 : index;
this.activePage = this.pageList[index].fullPath;
this.$router.push(this.activePage);
}
},
closePageListener(event) {
const { closeRoute, nextRoute } = event.detail;
const closePath = typeof closeRoute === "string" ? closeRoute : closeRoute.path;
this.remove(closePath, nextRoute);
}
}
};
</script>
<style scoped lang="less">
.tabs-view-content {
position: relative;
}
</style>
```