寫(xiě)在前面
- 關(guān)于
vue組件緩存的用法很簡(jiǎn)單,官網(wǎng)教程 講解的很詳細(xì),關(guān)于vue組件緩存的帶來(lái)的弊端網(wǎng)上也有很多探坑的文章,最明顯的就是緩存下來(lái)的組件如果不做處理,激活的時(shí)候就會(huì)命中緩存,如果你這個(gè)時(shí)候希望有新的數(shù)據(jù)獲取,可能你需要在activated鉤子函數(shù)中做一些處理,當(dāng)然網(wǎng)上有一些做法是通過(guò)路由的元信息來(lái)做一些處理,如果對(duì)組件緩存原理深入了解就知道那些方法可能不能徹底解決問(wèn)題; - 很繁瑣,因?yàn)槲乙沧鲞^(guò),所以我不希望在每個(gè)緩存組件中都做處理,我更希望的是,我想隨意銷(xiāo)毀某個(gè)緩存組件,我想進(jìn)行的是向下緩存而不是向上緩存或者都緩存,舉個(gè)例子,現(xiàn)在有一個(gè)列表頁(yè),詳情頁(yè),詳情頁(yè)子頁(yè)面,我希望,我離開(kāi)子頁(yè)面的時(shí)候,子頁(yè)面銷(xiāo)毀,離開(kāi)詳情頁(yè)的時(shí)候,詳情頁(yè)銷(xiāo)毀;
- 現(xiàn)在這些都成為可能了,不是很難理解,但是需要你知道 vue 組件緩存 實(shí)現(xiàn)的過(guò)程,如果不理解,可以參考 vue 技術(shù)揭秘之 keep-alive,因?yàn)閷?shí)現(xiàn)過(guò)程是對(duì)緩存的逆操作,本文只會(huì)介紹組件銷(xiāo)毀的實(shí)現(xiàn),不會(huì)拓展緩存相關(guān)內(nèi)容。
demo 場(chǎng)景描述
- 組件注冊(cè)
全局注冊(cè)四個(gè)路由級(jí)別非嵌套的組件,包含name、template選項(xiàng),部分組件包含beforeRouteLeave選項(xiàng), 分別為 列表 1、2、3、4
components.png - 路由配置
額外添加的就是路由元信息meta,里面包含了兩個(gè)關(guān)鍵字段level和compName前者后面會(huì)說(shuō),后者是對(duì)應(yīng)的組件名稱(chēng),即取的是組件的name字段
routes.png - 全部配置信息,這里采用的是
vue混入
mixins.png -
頁(yè)面結(jié)構(gòu),頂部固定導(dǎo)航條,可以導(dǎo)航到對(duì)應(yīng)的列表
view.png - 現(xiàn)在點(diǎn)擊導(dǎo)航欄 1、2、3、4 之后查看
vue-devtools可以看到,列表 1、2、3 都被緩存下來(lái)了
unhandler-cache-result.png
需求描述
假設(shè)上述是一個(gè)層層嵌套邏輯,列表1 > 列表2 > 列表3 > 列表4 ,現(xiàn)在需要在返回的時(shí)候,依次銷(xiāo)毀低層級(jí)的組件,所謂低層級(jí)指的是相對(duì)嵌套較深的,例如列表4相對(duì)于列表1、2、3都是低層級(jí)。我們先來(lái)簡(jiǎn)單實(shí)現(xiàn)這樣的一種需求
初級(jí)緩存組件清除實(shí)現(xiàn)
- 在
demo場(chǎng)景描述之路由配置里面,我在元信息里面添加了一個(gè)level字段,這個(gè)字段是用來(lái)描述當(dāng)前組件的級(jí)別,level越高代表是深層嵌套的組件,從 1 起步;
component-level.png - 下面是具體去緩存的實(shí)現(xiàn),封裝的去緩存方法
// util.js
function inArray(ele, array) {
let i = array.indexOf(ele)
let o = {
include: i !== -1,
index: i
}
return o
}
/**
* @param {Obejct} to 目標(biāo)路由
* @param {Obejct} from 當(dāng)前路由
* @param {Function} next next 管道函數(shù)
* @param {VNode} vm 當(dāng)前組件實(shí)例
* @param {Boolean} manualDelete 是否要手動(dòng)移除緩存組件,彌補(bǔ)當(dāng)路由缺少 level 時(shí),清空組件緩存的不足
*/
function destroyComponent (to, from, next, vm, manualDelete = false) {
// 禁止向上緩存
if (
(
from &&
from.meta.level &&
to.meta.level &&
from.meta.level > to.meta.level
) ||
manualDelete
) {
const { data, parent, componentOptions, key } = vm.$vnode
if (vm.$vnode && data.keepAlive) {
if (parent && parent.componentInstance && parent.componentInstance.cache) {
if (componentOptions) {
const cacheCompKey = !key ?
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
:
key
const cache = parent.componentInstance.cache
const keys = parent.componentInstance.keys
const { include, index } = inArray(cacheCompKey, keys)
// 清除緩存 component'key
if (include && cache[cacheCompKey]) {
keys.splice(index, 1)
delete cache[cacheCompKey]
}
}
}
}
// 銷(xiāo)毀緩存組件
vm.$destroy()
}
next()
}
// 你可以把它掛載到 vue 原型上
Vue.prototype.$dc = destroyComponent
- 然后你在全局混入的
beforeRouteLeave鉤子函數(shù)里面執(zhí)行該方法了, 最后一個(gè)參數(shù)允許你在組件內(nèi)的beforeRouteLeave里面執(zhí)行該方法來(lái)直接銷(xiāo)毀當(dāng)前組件
remove-cache-method-1.png - 上述方法通過(guò)對(duì)比兩個(gè)組件之間級(jí)別(level),符合條件就會(huì)從緩存列表(cache, keys)中刪除緩存組件,并且會(huì)調(diào)用
$destroy方法徹底銷(xiāo)毀緩存。 - 雖然該方法能夠?qū)崿F(xiàn)上面的簡(jiǎn)單邏輯,也能實(shí)現(xiàn)手動(dòng)控制銷(xiāo)毀,但是有一些問(wèn)題存在:
- 手動(dòng)銷(xiāo)毀的時(shí)候,只能銷(xiāo)毀當(dāng)前組件,不能銷(xiāo)毀指定的某個(gè)緩存組件或者某些緩存組件
- 只會(huì)判斷目標(biāo)組件和當(dāng)前組件的級(jí)別關(guān)系,不能判斷在兩者之間緩存的組件是否要移除,例如,列表1、2、3 均緩存了,如果直接從列表3跳到列表1,那么列表2是沒(méi)有處理的,還是處于緩存狀態(tài)的;
- 邊界情況,即如果目標(biāo)組件和當(dāng)前組件以及一樣,當(dāng)前組件也不會(huì)銷(xiāo)毀,雖然你可以修正為
from.meta.level >= to.meta.level但是有時(shí)候可能需要這樣的信息是可配置的
清除緩存的進(jìn)階
- 為了解決上面的問(wèn)題,下面是一個(gè)新的方案:既支持路由級(jí)別組件緩存的清除,又支持能定向清除某個(gè)或者一組緩存組件,且允許你調(diào)整整個(gè)項(xiàng)目清除緩存的邏輯;
- 創(chuàng)建一個(gè)包含緩存存儲(chǔ)、配置以及清空方法的對(duì)象
// util.js
function inArray(ele, array) {
let i = array.indexOf(ele)
let o = {
include: i !== -1,
index: i
}
return o
}
function isArray (array) {
return Array.isArray(array)
}
const hasOwnProperty = Object.prototype.hasOwnProperty
function hasOwn (key, obj) {
return hasOwnProperty.call(obj, key)
}
// 創(chuàng)建管理緩存的對(duì)象
class manageCachedComponents {
constructor () {
this.mc_keepAliveKeys = []
this.mc_keepAliveCache = {}
this.mc_cachedParentComponent = {}
this.mc_cachedCompnentsInfo = {}
this.mc_removeCacheRule = {
// 默認(rèn)為 true,即代表會(huì)移除低于目標(biāo)組件路由級(jí)別的所有緩存組件,
// 否則如果當(dāng)前組件路由級(jí)別低于目標(biāo)組件路由級(jí)別,只會(huì)移除當(dāng)前緩存組件
removeAllLowLevelCacheComp: true,
// 邊界情況,默認(rèn)是 true, 如果當(dāng)前組件和目標(biāo)組件路由級(jí)別一樣,是否清除當(dāng)前緩存組件
removeSameLevelCacheComp: true
}
}
/**
* 添加緩存組件到緩存列表
* @param {Object} Vnode 當(dāng)前組件實(shí)例
*/
mc_addCacheComponentToCacheList (Vnode) {
const { mc_cachedCompnentsInfo } = this
const { $vnode, $route, includes } = Vnode
const { componentOptions, parent } = $vnode
const componentName = componentOptions.Ctor.options.name
const compName = `cache-com::${componentName}`
const { include } = inArray(componentName, includes)
if (parent && include && !hasOwn(compName, mc_cachedCompnentsInfo)) {
const { keys, cache } = parent.componentInstance
const key = !$vnode.key
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: $vnode.key
const routeLevel = $route.meta.level
mc_cachedCompnentsInfo[compName] = {
// 組件名稱(chēng)
componentName,
// 緩存組件的 key
key,
// 組件路由級(jí)別
routeLevel
}
// 所有緩存組件 key 的列表
this.mc_keepAliveKeys = keys
// 所有緩存組件 key-value 集合
this.mc_keepAliveCache = cache
// 所有緩存組件的父實(shí)例
this.mc_cachedParentComponent = parent
}
}
// 移除緩存 key
mc_removeCacheKey (key, keys) {
const { include, index } = inArray(key, keys)
if (include) {
return keys.splice(index, 1)
}
}
/**
* 從 keep-alive 實(shí)例的 cache 移除緩存組件并移除緩存 key
* @param {String} key 緩存組件的 key
* @param {String} componentName 要清除的緩存組件名稱(chēng)
*/
mc_removeCachedComponent (key, componentName) {
const { mc_keepAliveKeys, mc_cachedParentComponent, mc_cachedCompnentsInfo } = this
const { componentInstance } = mc_cachedParentComponent
// 緩存組件 keep-alive 的 cache 和 keys
const cacheList = componentInstance.cache
const keysList = componentInstance.keys
const { include } = inArray(key, keysList)
if (include && cacheList[key]) {
this.mc_removeCacheKey(key, keysList)
this.mc_removeCacheKey(key, mc_keepAliveKeys)
cacheList[key].componentInstance.$destroy()
delete cacheList[key]
delete mc_cachedCompnentsInfo[componentName]
}
}
/**
* 根據(jù)組件名稱(chēng)移除指定的組件
* @param {String|Array} componentName 要移除的組件名稱(chēng)或者名稱(chēng)列表
*/
mc_removeCachedByComponentName (componentName) {
if (!isArray(componentName) && typeof componentName !== 'string') {
throw new TypeError(`移除的組件可以是 array 或者 string,當(dāng)前類(lèi)型為: ${typeof componentName}`)
}
const { mc_cachedCompnentsInfo } = this
if (isArray(componentName)) {
const unKnowComponents = []
for (const name of componentName) {
const compName = `cache-com::${name}`
if (hasOwn(compName, mc_cachedCompnentsInfo)) {
const { key } = mc_cachedCompnentsInfo[compName]
this.mc_removeCachedComponent(key, compName)
} else {
unKnowComponents.push(name)
}
}
// 提示存在非緩存組件
if (unKnowComponents.length) {
let tips = unKnowComponents.join(` && `)
console.warn(`${tips} 組件非緩存組件,請(qǐng)?jiān)谝瞥彺媪斜碇袆h除以上組件名`)
}
return
}
const compName = `cache-com::${componentName}`
if (hasOwn(compName, mc_cachedCompnentsInfo)) {
const { key } = mc_cachedCompnentsInfo[compName]
this.mc_removeCachedComponent(key, compName)
} else {
console.warn(`${componentName} 組件非緩存組件,請(qǐng)?zhí)砑诱_的緩存組件名`)
}
}
/**
* 移除路由級(jí)別的緩存組件
* @param {Object} toRoute 跳轉(zhuǎn)路由記錄
* @param {Object} Vnode 當(dāng)前組件實(shí)例
*/
mc_removeCachedByComponentLevel (toRoute, Vnode) {
const { level, compName } = toRoute.meta
const { mc_cachedCompnentsInfo, mc_removeCacheRule } = this
const componentName = Vnode.$vnode.componentOptions.Ctor.options.name
// exp-1-目標(biāo)組件非緩存組件,不做處理,但可以根據(jù)業(yè)務(wù)邏輯結(jié)合 removeCachedByComponentName 函數(shù)來(lái)處理
// exp-2-目標(biāo)組件是緩存組件,但是未添加 level,會(huì)默認(rèn)你一直緩存,不做處理
// exp-3-當(dāng)前組件非緩存組件,目標(biāo)組件為緩存組件,不做處理,參考 exp-1 的做法
// 以下邏輯只確保是兩個(gè)緩存組件之間的跳轉(zhuǎn)
if (
level &&
compName &&
mc_cachedCompnentsInfo['cache-com::' + compName] &&
mc_cachedCompnentsInfo['cache-com::' + componentName]
) {
const { removeAllLowLevelCacheComp, removeSameLevelCacheComp } = mc_removeCacheRule
if (removeAllLowLevelCacheComp) {
const cachedCompList = []
// 查找所有不小于當(dāng)前組件路由級(jí)別的緩存組件,即代表要銷(xiāo)毀的組件
for (const cacheItem in mc_cachedCompnentsInfo) {
const { componentName, routeLevel } = mc_cachedCompnentsInfo[cacheItem]
if (
// 排除目標(biāo)緩存組件,不希望目標(biāo)組件也被刪除
// 雖然會(huì)在 activated 鉤子函數(shù)里面重新添加到緩存列表
componentName !== compName &&
Number(routeLevel) >= level &&
// 邊界處理
removeSameLevelCacheComp
) {
cachedCompList.push(mc_cachedCompnentsInfo[cacheItem])
}
}
if (cachedCompList.length) {
cachedCompList.forEach(cacheItem => {
const { key, componentName } = cacheItem
const compName = 'cache-com::' + componentName
this.mc_removeCachedComponent(key, compName)
})
}
return
}
// 只移除當(dāng)前緩存組件
const { routeLevel } = mc_cachedCompnentsInfo['cache-com::' + componentName]
if (Number(routeLevel) >= level && removeSameLevelCacheComp) {
this.mc_removeCachedByComponentName(componentName)
}
}
}
}
// 你可以把它掛載到 vue 原型上
Vue.prototype.$mc = new manageCachedComponents()
- 使用起來(lái)非常簡(jiǎn)單,只需要你在全局的
activated函數(shù)里面執(zhí)行添加緩存方法,在全局beforeRouteLeave里面執(zhí)行移除方法方法即可
remove-cache-method-2.png
你還可以在組件內(nèi)的beforeRouteLeave鉤子函數(shù)里面執(zhí)行移除某些組件的邏輯
remove-custom-cache.png - 使用上述方法需要注意的事項(xiàng)是
- 給緩存組件添加組件名稱(chēng);
- 需要在路由記錄里面配置好
compName選項(xiàng),并且組織好你的level,因?yàn)樵趯?shí)際業(yè)務(wù)比demo復(fù)雜很多; - 緩存組件會(huì)激活
activated鉤子,你需要在該函數(shù)里面執(zhí)行添加緩存的方法,不然整個(gè)清緩存是不起作用的; - 默認(rèn)的清除規(guī)則是移除所有低層級(jí)的緩存組件(即緩存組件列表1、2、3,從列表3跳到列表1,列表2、3均會(huì)清除);
- 邊界情況的也會(huì)清除(即如果列表2、3 的
level相同,從列表3跳到列表2,會(huì)清除列表3的緩存);
- 你可能注意到了一個(gè)問(wèn)題,在整個(gè)項(xiàng)目中配置不支持動(dòng)態(tài)修改的,即在整個(gè)項(xiàng)目中緩存移除的規(guī)則是不同時(shí)支持兩種模式的,不想麻煩做是因?yàn)?
vue混入的緣故,全局的beforeRouteLeave會(huì)在組件內(nèi)beforeRouteLeave之前執(zhí)行,所以你懂得...不過(guò)你無(wú)需擔(dān)心有死角的清除問(wèn)題,因?yàn)槟憧梢酝ㄟ^(guò)mc_removeCachedByComponentName該方法來(lái)清除任意你想要銷(xiāo)毀的組件。
2019/05/04 - 新增對(duì) TS 支持
- 如果你是
vue + ts的開(kāi)發(fā)方式,可以采用下面的方式,由于當(dāng)前vue(或者 <2.6.10)的版本對(duì)ts支持不是很好,所以大部分是采用vue-shims.d.ts的方式來(lái)進(jìn)行模塊拓展,更多的使用細(xì)節(jié)可參考 vue 官網(wǎng)對(duì) Typescript 的支持 以及 Typescript 模塊拓展 -
下面是文件相對(duì)位置關(guān)系
file.png - vue-shims.d.ts 文件內(nèi)容
/*
* @description: 模塊拓展類(lèi)型定義文件
*/
import Vue, { VNode } from 'vue'
import { Route } from 'vue-router'
import ManageCachedComponents from './clear-cache'
export type ElementType = string | number
export interface KeepAliveCachedComponent {
[key: string]: VNode
}
interface CtorOptions {
name: string
[key: string]: any
}
declare module 'vue/types/vue' {
interface Vue {
$route: Route
$mc: ManageCachedComponents
includes: string[]
keys?: ElementType[]
cache?: KeepAliveCachedComponent
}
interface VueConstructor {
cid: number
options: CtorOptions
}
}
- cache-clear.ts 文件
/*
* @description: TS 版本的緩存移除
*/
import Vue, { VNode } from 'vue'
import { Route } from 'vue-router'
import { ElementType } from './vue-shim'
interface CachedComponentList {
componentName: string,
key: string,
routeLevel: number
}
interface RemoveCachedRules {
removeAllLowLevelCacheComp: boolean
removeSameLevelCacheComp: boolean
}
const hasOwnProperty = Object.prototype.hasOwnProperty
const inArray = (ele: ElementType, array: ElementType[]) => {
const i = array.indexOf(ele)
const o = {
include: i !== -1,
index: i
}
return o
}
const isArray = (array: any) => {
return Array.isArray(array)
}
const hasOwn = (key: ElementType, obj: object) => {
return hasOwnProperty.call(obj, key)
}
export default class ManageCachedComponents {
private mc_keepAliveKeys: ElementType[] = []
private mc_cachedParentComponent: VNode = <VNode>{}
private mc_cachedComponentsInfo: CachedComponentList = <CachedComponentList>{}
public mc_removeCacheRule: RemoveCachedRules = {
removeAllLowLevelCacheComp: true,
removeSameLevelCacheComp: true
}
/**
* 從緩存列表中移除 key
*/
private mc_removeCacheKey (key: ElementType, keys: ElementType[]) {
const { include, index } = inArray(key, keys)
include && keys.splice(index, 1)
}
/**
* 從 keep-alive 實(shí)例的 cache 移除緩存組件并移除緩存 key
* @param key 緩存組件的 key
* @param componentName 要清除的緩存組件名稱(chēng)
*/
private mc_removeCachedComponent (key: string, componentName: string) {
const { mc_keepAliveKeys, mc_cachedParentComponent, mc_cachedComponentsInfo } = this
const { componentInstance } = mc_cachedParentComponent
const cacheList = componentInstance.cache
const keysList = componentInstance.keys
const { include } = inArray(key, keysList)
if (include && cacheList[key]) {
this.mc_removeCacheKey(key, keysList)
this.mc_removeCacheKey(key, mc_keepAliveKeys)
cacheList[key].componentInstance.$destroy()
delete cacheList[key]
delete mc_cachedComponentsInfo[componentName]
}
}
/**
* 添加緩存組件到緩存列表
* @param Vue 當(dāng)前組件實(shí)例
*/
mc_addCacheComponentToCacheList (Vue: Vue) {
const { mc_cachedComponentsInfo } = this
const { $vnode, $route, includes } = Vue
const { componentOptions, parent } = $vnode
const componentName = componentOptions.Ctor.options.name
const compName = `cache-com::${componentName}`
const { include } = inArray(componentName, includes)
if (parent && include && !hasOwn(compName, mc_cachedComponentsInfo)) {
const { keys, cache } = parent.componentInstance
const key = !$vnode.key
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: $vnode.key
const routeLevel = $route.meta.level
mc_cachedComponentsInfo[compName] = {
componentName,
key,
routeLevel
}
this.mc_keepAliveKeys = keys
this.mc_cachedParentComponent = parent
}
}
/**
* 根據(jù)組件名稱(chēng)移除指定的組件
* @param componentName 要移除的組件名稱(chēng)或者名稱(chēng)列表
*/
mc_removeCachedByComponentName (componentName: string | string[]) {
if (!isArray(componentName) && typeof componentName !== 'string') {
throw new TypeError(`移除的組件可以是 array 或者 string,當(dāng)前類(lèi)型為: ${typeof componentName}`)
}
const { mc_cachedComponentsInfo } = this
if (isArray(componentName)) {
const unKnowComponents = []
for (const name of componentName) {
const compName = `cache-com::${name}`
if (hasOwn(compName, mc_cachedComponentsInfo)) {
const { key } = mc_cachedComponentsInfo[compName]
this.mc_removeCachedComponent(key, compName)
} else {
unKnowComponents.push(name)
}
}
// 提示存在非緩存組件
if (unKnowComponents.length) {
let tips = unKnowComponents.join(` && `)
console.warn(`${tips} 組件非緩存組件,請(qǐng)?jiān)谝瞥彺媪斜碇袆h除以上組件名`)
}
return
}
const compName = `cache-com::${componentName}`
if (hasOwn(compName, mc_cachedComponentsInfo)) {
const { key } = mc_cachedComponentsInfo[compName]
this.mc_removeCachedComponent(key, compName)
} else {
console.warn(`${componentName} 組件非緩存組件,請(qǐng)?zhí)砑诱_的緩存組件名`)
}
}
/**
* 移除路由級(jí)別的緩存組件
* @param toRoute 跳轉(zhuǎn)路由記錄
* @param Vue 當(dāng)前組件實(shí)例
*/
mc_removeCachedByComponentLevel (toRoute: Route, Vue: Vue) {
const { level, compName } = toRoute.meta
const { mc_cachedComponentsInfo, mc_removeCacheRule } = this
const componentName = Vue.$vnode.componentOptions.Ctor.options.name
if (
level &&
compName &&
mc_cachedComponentsInfo['cache-com::' + compName] &&
mc_cachedComponentsInfo['cache-com::' + componentName]
) {
const { removeAllLowLevelCacheComp, removeSameLevelCacheComp } = mc_removeCacheRule
if (removeAllLowLevelCacheComp) {
const cachedCompList = []
for (const cacheItem in mc_cachedComponentsInfo) {
const { componentName, routeLevel } = mc_cachedComponentsInfo[cacheItem]
if (
componentName !== compName &&
Number(routeLevel) >= level &&
removeSameLevelCacheComp
) {
cachedCompList.push(mc_cachedComponentsInfo[cacheItem])
}
}
if (cachedCompList.length) {
cachedCompList.forEach(cacheItem => {
const { key, componentName } = cacheItem
const compName = 'cache-com::' + componentName
this.mc_removeCachedComponent(key, compName)
})
}
return
}
const { routeLevel } = mc_cachedComponentsInfo['cache-com::' + componentName]
if (Number(routeLevel) >= level && removeSameLevelCacheComp) {
this.mc_removeCachedByComponentName(componentName)
}
}
}
}
- 如果
vue3.0出來(lái)以后,就不需要vue-shims.d.ts文件了,到時(shí)候使用ts會(huì)更加方便,當(dāng)然更希望尤大能夠增加緩存操作的api,這樣就不再為了緩存而造各種輪子了。
寫(xiě)在最后
- 這篇文章主要參考的是 vue組件緩存源碼,感興趣的可以看一下;
- 本文為原創(chuàng)文章,如果需要轉(zhuǎn)載,請(qǐng)注明出處,方便溯源,如有錯(cuò)誤地方,可以在下方留言,歡迎斧正,
demo已經(jīng)上傳到 關(guān)于vue緩存清除的個(gè)人git倉(cāng)庫(kù)









