本文是對(duì) vue-element-admin 源碼研究,根據(jù)項(xiàng)目中緩存方面和 Tagviews 實(shí)現(xiàn),進(jìn)行改進(jìn),同時(shí)研究 Vue 內(nèi)置組件
keep-alive的用法和存在問題。
基礎(chǔ)
其中需要注意以下幾點(diǎn):
keep-alive 本質(zhì)是把應(yīng)該銷毀的組件緩存起來,當(dāng)再次需要的時(shí)候去讀取緩存的組件信息而不是重新渲染,所以 keep-alive 必須包裹一個(gè)組件才能生效。
使用了
includeandexclude會(huì)按照這個(gè)規(guī)則進(jìn)行匹配緩存那些頁面,不使用會(huì)緩存所有。如果使用了第二條的篩選規(guī)則,那么必須配置對(duì)照和
name,不然無法正確緩存。
文檔原句:
匹配首先檢查組件自身的 name 選項(xiàng),如果 name 選項(xiàng)不可用,則匹配它的局部注冊(cè)名稱 (父組件 components 選項(xiàng)的鍵值)。匿名組件不能被匹配。
- keep-alive 內(nèi)部的
router-view,填寫 key 的時(shí)候,需要謹(jǐn)慎 ,不然會(huì)出現(xiàn)問題。
比如在編輯信息的時(shí)候,用戶打開了兩個(gè)標(biāo)簽頁使用了同一個(gè)組件,不使用 key 就會(huì)復(fù)用這同一個(gè)組件 但是我們需要的是渲染兩個(gè),使用不同的 key 就會(huì)分別渲染兩個(gè),而有時(shí)候 key 又會(huì)生成多余的頁面。
- 取消緩存頁面只需要把
includeandexclude中不需要緩存的name刪除即可,因?yàn)樵创a中會(huì)監(jiān)聽這個(gè)兩個(gè)字段,刪除緩存的組件。
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 中的緩存
默認(rèn)只實(shí)現(xiàn)了一層緩存,對(duì)緩存頁面進(jìn)行刷新、刪除等操作。
定一個(gè)目標(biāo)
- 實(shí)現(xiàn)多層嵌套下,對(duì)頁面進(jìn)行緩存,同時(shí)可以進(jìn)行刪除、刷新。
- 動(dòng)態(tài)路由 可打開多個(gè)并同時(shí)進(jìn)行分別緩存。
開始
本篇使用
include對(duì)緩存頁面進(jìn)行新增和刪除,不考慮默認(rèn)全部緩存的情況
嵌套緩存的實(shí)現(xiàn)
本文例子使用了三層路由:
App.vue、Main.vue(布局) 、其他第三層路由,只有第二層和第三層啟動(dòng)了緩存,稱為 第一層緩存和第二層緩存 。
緩存路由樹的實(shí)現(xiàn)
參照了 vue-element-admin 中 tagsViews 的實(shí)現(xiàn)在 Vuex 中生成了一個(gè)一維數(shù)組,實(shí)現(xiàn)一層緩存。
https://github.com/PanJiaChen/vue-element-admin/blob/v4.0.0/src/store/modules/tagsView.js
想要實(shí)現(xiàn)多層嵌套緩存 必須建立多維數(shù)組
經(jīng)過實(shí)驗(yàn)和思考后使用 this.$route.matched 對(duì)路由信息進(jìn)行轉(zhuǎn)化為樹形結(jié)構(gòu)
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;
}
一個(gè)節(jié)點(diǎn)的數(shù)據(jù)信息為
{
name: "", //組件的name 主要用于 inclues
path: "", // 區(qū)分相同 name 的 頁面
many:boolean,//是否是動(dòng)態(tài)路由
children:[], //子類
parent: [] // 父類映射,用于刪除和修改,每次修改刪除都遍歷整個(gè)樹太消耗性能了
}
每次切換頁面都會(huì)生成一個(gè)當(dāng)前路由信息的 單分支樹 與總樹進(jìn)行 diff 合并或刪除
新增
/**
* 新增一個(gè)緩存節(jié)點(diǎn)
*/
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 {
//如果是動(dòng)態(tài)路由則可以添加多個(gè),在銷毀的時(shí)候只有全部關(guān)閉才會(huì)取消緩存
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);
}
}
}
}
在使用的時(shí)候根據(jù)這一棵總數(shù)獲取想要的樹形獲取想要的數(shù)據(jù),比如第一層節(jié)點(diǎn) 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 需要另行封裝,但是項(xiàng)目中最多也就是實(shí)現(xiàn)三層路由,進(jìn)行兩層緩存,所以目前不考慮。
到此緩存數(shù)據(jù)樹已經(jīng)實(shí)現(xiàn),但是在頁面中的操作還有很多坑,和其解決思路。
頁面中的設(shè)置
在第一層緩存中使用 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,因?yàn)槲覀儺?dāng)前是第二層路由,第一層是 App.vue , 如果等于第二層就取當(dāng)前的路由 path
在第二層緩存中使用 Vuex Getter 的函數(shù)形式獲取確定的緩存頁面 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>
在第二層的緩存的時(shí)候,key值處理比較復(fù)雜,原本是直接使用this.$route.path,但是出現(xiàn)了非常致命的問題。
主要原因是:
Vue 緩存的頁面,由于屬性劫持的原因,即使被緩存了,
$route的變化還會(huì)觸發(fā)變化,$route變化,觸發(fā)了 key 的變化 從而制造多余無意義的頁面如下:

組件被緩存后,由于 key 值綁定 $route.path 當(dāng)頁面切換時(shí),key發(fā)生改變會(huì)創(chuàng)建大量的無用頁面占用內(nèi)存,導(dǎo)致頁面迅速卡死。
所以引出一個(gè)問題,緩存的頁面是否需要繼續(xù)活躍屬性變化,但是數(shù)據(jù)劫持是 Vue 的核心,目前沒有任何辦法能從根源解決,即,短時(shí)間凍結(jié)劫持。
目前解決方法是在第三層 <route-view /> 中緩存 key ,只有當(dāng)前頁面切換是當(dāng)前的緩存的子頁面才會(huì)改變 key。
小結(jié)
通過這種方式,可以在一定程度上實(shí)現(xiàn)多層緩存和刪除,但是如果牽扯到緩存的刷新和動(dòng)態(tài)路由緩存等問題,就會(huì)發(fā)現(xiàn) keep-alive 存在的很多缺陷,下面會(huì)一一介紹.
當(dāng)前思路下其他的嵌套緩存方案(廢棄)
在嘗試嵌套緩存的時(shí)候,還進(jìn)行了其他的嘗試:
這種方案本質(zhì)是 直接在
vue-element-admin緩存方案中直接套用 嵌套緩存,并非參照系統(tǒng)的本身問題,因?yàn)?vue-element-admin 本身需求就是緩存一層。
這種方案本質(zhì)還是在于 key 的處理上 ,在上文的基礎(chǔ)上進(jìn)行一點(diǎn)點(diǎn)修改:
- 在第一層緩存中 key 值總是取最底層的 path 即
this.$route.path,試想一下,無論是二層嵌套路由或者是三層嵌套路由,永遠(yuǎn)都是最底層的 path ,表現(xiàn)結(jié)果是:

由上圖可以看到 造成了更加嚴(yán)重的性能問題!
有兩點(diǎn)困難之處:
- 上文說的 緩存頁面內(nèi)部的劫持依然活躍 key 的變化創(chuàng)造了更多的無用頁面。
- 由于每一個(gè)二級(jí)緩存都創(chuàng)建了
AuthorityAuth組件, 也就造成了 無法刪除緩存,因?yàn)樗鼈兊?name 都是AuthorityAuth,刪除一個(gè)就換導(dǎo)致全部緩存刪除。
keep-alive 確定緩存是以 name 為基準(zhǔn)的 ,這導(dǎo)致在在一個(gè)組件創(chuàng)建不同的 key 達(dá)到 復(fù)用,比如緩存多個(gè)動(dòng)態(tài)路由 ,無法精準(zhǔn)的刪除某一個(gè)頁面。
動(dòng)態(tài)組件緩存問題
這個(gè)問題和上一段寫的問題是同一個(gè),由于動(dòng)態(tài)路由,使用的同一個(gè)組件,name 是相同的,我們可以通過 key 打開多個(gè)頁面,但是我們卻沒辦法精準(zhǔn)的控制每個(gè)頁面的緩存和刷新。
我們只能實(shí)現(xiàn):全部關(guān)閉后全部清空。
遺留的問題還有:一個(gè)刷新,則全部刷新
目前實(shí)現(xiàn)是打開多個(gè)無法刷新,因?yàn)?,為了?shí)現(xiàn)全部關(guān)閉后取消緩存,也就是說在緩存樹中會(huì)創(chuàng)建多個(gè) name 相同,但是 path 不同的緩存信息,最后再去重得到 include。
其他緩存思路
網(wǎng)上還有很多大佬有很多的想法來實(shí)現(xiàn)緩存頁面,大致可分:
默認(rèn)緩存所有,手動(dòng)調(diào)用
vm.$destroy()注銷組件。通過查詢 Vnode 找到 keep-alive 的 cache 手動(dòng)刪除緩存。
不使用 keep-alive 頁面切換保存 data 的屬性。
等等等。。。。
但是我感覺還是使用 keep-alive 比較好,但是 keep-alive 擁有兩個(gè)缺陷 。
keep-alive 的局限性
緩存的頁面內(nèi)部使用的劫持屬性還是活躍的,這會(huì)導(dǎo)致其他頁面的操作影響緩存的頁面,比如 key 值綁定問題。
keep-alive 在緩存 動(dòng)態(tài)路由的問題,相同的 name 可以使用 key 創(chuàng)建不同的 實(shí)例,但是我們只能用 name 去操作這一系列頁面 。
總結(jié)
如果不考慮以上出現(xiàn)的問題,那么本文還是可以解決,一般遇到的所有緩存問題。
參考資料
Vue Key
一句話來說就是不同的 key 會(huì)被 Vue 當(dāng)成不同的元素,即使是使用了相同的組件,會(huì)被創(chuàng)建多份,這在配合路由和緩存使用時(shí)尤其重要。