>vue使用頁簽模式,組件使用keep-alive緩存,發現頁簽關閉后緩存組件未銷毀,只是出于非活動狀態
解決方案
* 復制一份keep-alive源碼并修改,關閉頁簽時傳入需要刪除緩存的tag,與cache對比,刪除cache[tag]
](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>
```