寫在前面
- 關(guān)于
vue
組件緩存的用法很簡單,官網(wǎng)教程 講解的很詳細(xì),關(guān)于vue
組件緩存的帶來的弊端網(wǎng)上也有很多探坑的文章,最明顯的就是緩存下來的組件如果不做處理,激活的時候就會命中緩存八秃,如果你這個時候希望有新的數(shù)據(jù)獲取,可能你需要在activated
鉤子函數(shù)中做一些處理肉盹,當(dāng)然網(wǎng)上有一些做法是通過路由的元信息來做一些處理昔驱,如果對組件緩存原理深入了解就知道那些方法可能不能徹底解決問題; - 很繁瑣上忍,因為我也做過骤肛,所以我不希望在每個緩存組件中都做處理,我更希望的是窍蓝,我想隨意銷毀某個緩存組件腋颠,我想進(jìn)行的是向下緩存而不是向上緩存或者都緩存,舉個例子吓笙,現(xiàn)在有一個列表頁淑玫,詳情頁,詳情頁子頁面面睛,我希望絮蒿,我離開子頁面的時候,子頁面銷毀叁鉴,離開詳情頁的時候土涝,詳情頁銷毀;
- 現(xiàn)在這些都成為可能了幌墓,不是很難理解回铛,但是需要你知道 vue 組件緩存 實現(xiàn)的過程,如果不理解克锣,可以參考 vue 技術(shù)揭秘之 keep-alive,因為實現(xiàn)過程是對緩存的逆操作腔长,本文只會介紹組件銷毀的實現(xiàn)袭祟,不會拓展緩存相關(guān)內(nèi)容。
demo 場景描述
- 組件注冊
全局注冊四個路由級別非嵌套的組件捞附,包含name
巾乳、template
選項您没,部分組件包含beforeRouteLeave
選項, 分別為 列表 1胆绊、2氨鹏、3、4
components.png - 路由配置
額外添加的就是路由元信息meta
压状,里面包含了兩個關(guān)鍵字段level
和compName
前者后面會說仆抵,后者是對應(yīng)的組件名稱,即取的是組件的name
字段
routes.png - 全部配置信息种冬,這里采用的是
vue
混入
mixins.png -
頁面結(jié)構(gòu)镣丑,頂部固定導(dǎo)航條,可以導(dǎo)航到對應(yīng)的列表
view.png - 現(xiàn)在點擊導(dǎo)航欄 1娱两、2莺匠、3、4 之后查看
vue-devtools
可以看到十兢,列表 1趣竣、2、3 都被緩存下來了
unhandler-cache-result.png
需求描述
假設(shè)上述是一個層層嵌套邏輯旱物,列表1 > 列表2 > 列表3 > 列表4 遥缕,現(xiàn)在需要在返回的時候,依次銷毀低層級的組件异袄,所謂低層級指的是相對嵌套較深的通砍,例如列表4相對于列表1、2烤蜕、3都是低層級封孙。我們先來簡單實現(xiàn)這樣的一種需求
初級緩存組件清除實現(xiàn)
- 在
demo
場景描述之路由配置里面,我在元信息里面添加了一個level
字段讽营,這個字段是用來描述當(dāng)前組件的級別虎忌,level
越高代表是深層嵌套的組件,從 1 起步橱鹏;
component-level.png - 下面是具體去緩存的實現(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)前組件實例
* @param {Boolean} manualDelete 是否要手動移除緩存組件,彌補當(dāng)路由缺少 level 時莉兰,清空組件緩存的不足
*/
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]
}
}
}
}
// 銷毀緩存組件
vm.$destroy()
}
next()
}
// 你可以把它掛載到 vue 原型上
Vue.prototype.$dc = destroyComponent
- 然后你在全局混入的
beforeRouteLeave
鉤子函數(shù)里面執(zhí)行該方法了挑围, 最后一個參數(shù)允許你在組件內(nèi)的beforeRouteLeave
里面執(zhí)行該方法來直接銷毀當(dāng)前組件
remove-cache-method-1.png - 上述方法通過對比兩個組件之間級別(level),符合條件就會從緩存列表(cache, keys)中刪除緩存組件糖荒,并且會調(diào)用
$destroy
方法徹底銷毀緩存杉辙。 - 雖然該方法能夠?qū)崿F(xiàn)上面的簡單邏輯,也能實現(xiàn)手動控制銷毀捶朵,但是有一些問題存在:
- 手動銷毀的時候蜘矢,只能銷毀當(dāng)前組件狂男,不能銷毀指定的某個緩存組件或者某些緩存組件
- 只會判斷目標(biāo)組件和當(dāng)前組件的級別關(guān)系,不能判斷在兩者之間緩存的組件是否要移除品腹,例如岖食,列表1、2舞吭、3 均緩存了泡垃,如果直接從列表3跳到列表1,那么列表2是沒有處理的镣典,還是處于緩存狀態(tài)的兔毙;
- 邊界情況,即如果目標(biāo)組件和當(dāng)前組件以及一樣兄春,當(dāng)前組件也不會銷毀澎剥,雖然你可以修正為
from.meta.level >= to.meta.level
但是有時候可能需要這樣的信息是可配置的
清除緩存的進(jìn)階
- 為了解決上面的問題,下面是一個新的方案:既支持路由級別組件緩存的清除赶舆,又支持能定向清除某個或者一組緩存組件哑姚,且允許你調(diào)整整個項目清除緩存的邏輯;
- 創(chuàng)建一個包含緩存存儲芜茵、配置以及清空方法的對象
// 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)建管理緩存的對象
class manageCachedComponents {
constructor () {
this.mc_keepAliveKeys = []
this.mc_keepAliveCache = {}
this.mc_cachedParentComponent = {}
this.mc_cachedCompnentsInfo = {}
this.mc_removeCacheRule = {
// 默認(rèn)為 true叙量,即代表會移除低于目標(biāo)組件路由級別的所有緩存組件,
// 否則如果當(dāng)前組件路由級別低于目標(biāo)組件路由級別九串,只會移除當(dāng)前緩存組件
removeAllLowLevelCacheComp: true,
// 邊界情況绞佩,默認(rèn)是 true, 如果當(dāng)前組件和目標(biāo)組件路由級別一樣猪钮,是否清除當(dāng)前緩存組件
removeSameLevelCacheComp: true
}
}
/**
* 添加緩存組件到緩存列表
* @param {Object} Vnode 當(dāng)前組件實例
*/
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] = {
// 組件名稱
componentName,
// 緩存組件的 key
key,
// 組件路由級別
routeLevel
}
// 所有緩存組件 key 的列表
this.mc_keepAliveKeys = keys
// 所有緩存組件 key-value 集合
this.mc_keepAliveCache = cache
// 所有緩存組件的父實例
this.mc_cachedParentComponent = parent
}
}
// 移除緩存 key
mc_removeCacheKey (key, keys) {
const { include, index } = inArray(key, keys)
if (include) {
return keys.splice(index, 1)
}
}
/**
* 從 keep-alive 實例的 cache 移除緩存組件并移除緩存 key
* @param {String} key 緩存組件的 key
* @param {String} componentName 要清除的緩存組件名稱
*/
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ù)組件名稱移除指定的組件
* @param {String|Array} componentName 要移除的組件名稱或者名稱列表
*/
mc_removeCachedByComponentName (componentName) {
if (!isArray(componentName) && typeof componentName !== 'string') {
throw new TypeError(`移除的組件可以是 array 或者 string品山,當(dāng)前類型為: ${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} 組件非緩存組件,請在移除緩存列表中刪除以上組件名`)
}
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} 組件非緩存組件烤低,請?zhí)砑诱_的緩存組件名`)
}
}
/**
* 移除路由級別的緩存組件
* @param {Object} toRoute 跳轉(zhuǎn)路由記錄
* @param {Object} Vnode 當(dāng)前組件實例
*/
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ù)來處理
// exp-2-目標(biāo)組件是緩存組件扑馁,但是未添加 level涯呻,會默認(rèn)你一直緩存,不做處理
// exp-3-當(dāng)前組件非緩存組件腻要,目標(biāo)組件為緩存組件复罐,不做處理,參考 exp-1 的做法
// 以下邏輯只確保是兩個緩存組件之間的跳轉(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)前組件路由級別的緩存組件雄家,即代表要銷毀的組件
for (const cacheItem in mc_cachedCompnentsInfo) {
const { componentName, routeLevel } = mc_cachedCompnentsInfo[cacheItem]
if (
// 排除目標(biāo)緩存組件市栗,不希望目標(biāo)組件也被刪除
// 雖然會在 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()
- 使用起來非常簡單,只需要你在全局的
activated
函數(shù)里面執(zhí)行添加緩存方法,在全局beforeRouteLeave
里面執(zhí)行移除方法方法即可
remove-cache-method-2.png
你還可以在組件內(nèi)的beforeRouteLeave
鉤子函數(shù)里面執(zhí)行移除某些組件的邏輯
remove-custom-cache.png - 使用上述方法需要注意的事項是
- 給緩存組件添加組件名稱填帽;
- 需要在路由記錄里面配置好
compName
選項,并且組織好你的level
咙好,因為在實際業(yè)務(wù)比demo
復(fù)雜很多篡腌; - 緩存組件會激活
activated
鉤子,你需要在該函數(shù)里面執(zhí)行添加緩存的方法勾效,不然整個清緩存是不起作用的嘹悼; - 默認(rèn)的清除規(guī)則是移除所有低層級的緩存組件(即緩存組件列表1、2层宫、3杨伙,從列表3跳到列表1,列表2萌腿、3均會清除)限匣;
- 邊界情況的也會清除(即如果列表2、3 的
level
相同毁菱,從列表3跳到列表2米死,會清除列表3的緩存);
- 你可能注意到了一個問題贮庞,在整個項目中配置不支持動態(tài)修改的峦筒,即在整個項目中緩存移除的規(guī)則是不同時支持兩種模式的,不想麻煩做是因為
vue
混入的緣故窗慎,全局的beforeRouteLeave
會在組件內(nèi)beforeRouteLeave
之前執(zhí)行物喷,所以你懂得...不過你無需擔(dān)心有死角的清除問題,因為你可以通過mc_removeCachedByComponentName
該方法來清除任意你想要銷毀的組件遮斥。
2019/05/04 - 新增對 TS 支持
- 如果你是
vue + ts
的開發(fā)方式峦失,可以采用下面的方式,由于當(dāng)前vue
(或者 <2.6.10)的版本對ts
支持不是很好伏伐,所以大部分是采用vue-shims.d.ts
的方式來進(jìn)行模塊拓展宠进,更多的使用細(xì)節(jié)可參考 vue 官網(wǎng)對 Typescript 的支持 以及 Typescript 模塊拓展 -
下面是文件相對位置關(guān)系
file.png - vue-shims.d.ts 文件內(nèi)容
/*
* @description: 模塊拓展類型定義文件
*/
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 實例的 cache 移除緩存組件并移除緩存 key
* @param key 緩存組件的 key
* @param componentName 要清除的緩存組件名稱
*/
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)前組件實例
*/
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ù)組件名稱移除指定的組件
* @param componentName 要移除的組件名稱或者名稱列表
*/
mc_removeCachedByComponentName (componentName: string | string[]) {
if (!isArray(componentName) && typeof componentName !== 'string') {
throw new TypeError(`移除的組件可以是 array 或者 string,當(dāng)前類型為: ${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} 組件非緩存組件藐翎,請在移除緩存列表中刪除以上組件名`)
}
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} 組件非緩存組件材蹬,請?zhí)砑诱_的緩存組件名`)
}
}
/**
* 移除路由級別的緩存組件
* @param toRoute 跳轉(zhuǎn)路由記錄
* @param Vue 當(dāng)前組件實例
*/
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
出來以后,就不需要vue-shims.d.ts
文件了吝镣,到時候使用ts
會更加方便堤器,當(dāng)然更希望尤大能夠增加緩存操作的api
,這樣就不再為了緩存而造各種輪子了末贾。
寫在最后
- 這篇文章主要參考的是 vue組件緩存源碼闸溃,感興趣的可以看一下;
- 本文為原創(chuàng)文章,如果需要轉(zhuǎn)載辉川,請注明出處表蝙,方便溯源,如有錯誤地方乓旗,可以在下方留言府蛇,歡迎斧正,
demo
已經(jīng)上傳到 關(guān)于vue緩存清除的個人git倉庫