写在前面
- 关于
vue
组件缓存的用法很简单,官网教程 讲解的很详细,关于vue
组件缓存的带来的弊端网上也有很多探坑的文章,最明显的就是缓存下来的组件如果不做处理,激活的时候就会命中缓存,如果你这个时候希望有新的数据获取,可能你需要在activated
钩子函数中做一些处理,当然网上有一些做法是通过路由的元信息来做一些处理,如果对组件缓存原理深入了解就知道那些方法可能不能彻底解决问题; - 很繁琐,因为我也做过,所以我不希望在每个缓存组件中都做处理,我更希望的是,我想随意销毁某个缓存组件,我想进行的是向下缓存而不是向上缓存或者都缓存,举个例子,现在有一个列表页,详情页,详情页子页面,我希望,我离开子页面的时候,子页面销毁,离开详情页的时候,详情页销毁;
- 现在这些都成为可能了,不是很难理解,但是需要你知道 vue 组件缓存 实现的过程,如果不理解,可以参考 vue 技术揭秘之 keep-alive,因为实现过程是对缓存的逆操作,本文只会介绍组件销毁的实现,不会拓展缓存相关内容。
demo 场景描述
- 组件注册
全局注册四个路由级别非嵌套的组件,包含name
、template
选项,部分组件包含beforeRouteLeave
选项, 分别为 列表 1、2、3、4
- 路由配置
额外添加的就是路由元信息meta
,里面包含了两个关键字段level
和compName
前者后面会说,后者是对应的组件名称,即取的是组件的name
字段
- 全部配置信息,这里采用的是
vue
混入
-
页面结构,顶部固定导航条,可以导航到对应的列表
- 现在点击导航栏 1、2、3、4 之后查看
vue-devtools
可以看到,列表 1、2、3 都被缓存下来了
需求描述
假设上述是一个层层嵌套逻辑,列表1 > 列表2 > 列表3 > 列表4 ,现在需要在返回的时候,依次销毁低层级的组件,所谓低层级指的是相对嵌套较深的,例如列表4相对于列表1、2、3都是低层级。我们先来简单实现这样的一种需求
初级缓存组件清除实现
- 在
demo
场景描述之路由配置里面,我在元信息里面添加了一个level
字段,这个字段是用来描述当前组件的级别,level
越高代表是深层嵌套的组件,从 1 起步;
- 下面是具体去缓存的实现,封装的去缓存方法
// util.js
function inArray(ele, array) {
let i = array.indexOf(ele)
let o = {
include: i !== -1,
index: i
}
return o
}
/**
* @param {Obejct} to 目标路由
* @param {Obejct} from 当前路由
* @param {Function} next next 管道函数
* @param {VNode} vm 当前组件实例
* @param {Boolean} manualDelete 是否要手动移除缓存组件,弥补当路由缺少 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
钩子函数里面执行该方法了, 最后一个参数允许你在组件内的beforeRouteLeave
里面执行该方法来直接销毁当前组件
- 上述方法通过对比两个组件之间级别(level),符合条件就会从缓存列表(cache, keys)中删除缓存组件,并且会调用
$destroy
方法彻底销毁缓存。 - 虽然该方法能够实现上面的简单逻辑,也能实现手动控制销毁,但是有一些问题存在:
- 手动销毁的时候,只能销毁当前组件,不能销毁指定的某个缓存组件或者某些缓存组件
- 只会判断目标组件和当前组件的级别关系,不能判断在两者之间缓存的组件是否要移除,例如,列表1、2、3 均缓存了,如果直接从列表3跳到列表1,那么列表2是没有处理的,还是处于缓存状态的;
- 边界情况,即如果目标组件和当前组件以及一样,当前组件也不会销毁,虽然你可以修正为
from.meta.level >= to.meta.level
但是有时候可能需要这样的信息是可配置的
清除缓存的进阶
- 为了解决上面的问题,下面是一个新的方案:既支持路由级别组件缓存的清除,又支持能定向清除某个或者一组缓存组件,且允许你调整整个项目清除缓存的逻辑;
- 创建一个包含缓存存储、配置以及清空方法的对象
// 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)
}
// 创建管理缓存的对象
class manageCachedComponents {
constructor () {
this.mc_keepAliveKeys = []
this.mc_keepAliveCache = {}
this.mc_cachedParentComponent = {}
this.mc_cachedCompnentsInfo = {}
this.mc_removeCacheRule = {
// 默认为 true,即代表会移除低于目标组件路由级别的所有缓存组件,
// 否则如果当前组件路由级别低于目标组件路由级别,只会移除当前缓存组件
removeAllLowLevelCacheComp: true,
// 边界情况,默认是 true, 如果当前组件和目标组件路由级别一样,是否清除当前缓存组件
removeSameLevelCacheComp: true
}
}
/**
* 添加缓存组件到缓存列表
* @param {Object} Vnode 当前组件实例
*/
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]
}
}
/**
* 根据组件名称移除指定的组件
* @param {String|Array} componentName 要移除的组件名称或者名称列表
*/
mc_removeCachedByComponentName (componentName) {
if (!isArray(componentName) && typeof componentName !== 'string') {
throw new TypeError(`移除的组件可以是 array 或者 string,当前类型为: ${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} 组件非缓存组件,请添加正确的缓存组件名`)
}
}
/**
* 移除路由级别的缓存组件
* @param {Object} toRoute 跳转路由记录
* @param {Object} Vnode 当前组件实例
*/
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-目标组件非缓存组件,不做处理,但可以根据业务逻辑结合 removeCachedByComponentName 函数来处理
// exp-2-目标组件是缓存组件,但是未添加 level,会默认你一直缓存,不做处理
// exp-3-当前组件非缓存组件,目标组件为缓存组件,不做处理,参考 exp-1 的做法
// 以下逻辑只确保是两个缓存组件之间的跳转
if (
level &&
compName &&
mc_cachedCompnentsInfo['cache-com::' + compName] &&
mc_cachedCompnentsInfo['cache-com::' + componentName]
) {
const { removeAllLowLevelCacheComp, removeSameLevelCacheComp } = mc_removeCacheRule
if (removeAllLowLevelCacheComp) {
const cachedCompList = []
// 查找所有不小于当前组件路由级别的缓存组件,即代表要销毁的组件
for (const cacheItem in mc_cachedCompnentsInfo) {
const { componentName, routeLevel } = mc_cachedCompnentsInfo[cacheItem]
if (
// 排除目标缓存组件,不希望目标组件也被删除
// 虽然会在 activated 钩子函数里面重新添加到缓存列表
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
}
// 只移除当前缓存组件
const { routeLevel } = mc_cachedCompnentsInfo['cache-com::' + componentName]
if (Number(routeLevel) >= level && removeSameLevelCacheComp) {
this.mc_removeCachedByComponentName(componentName)
}
}
}
}
// 你可以把它挂载到 vue 原型上
Vue.prototype.$mc = new manageCachedComponents()
- 使用起来非常简单,只需要你在全局的
activated
函数里面执行添加缓存方法,在全局beforeRouteLeave
里面执行移除方法方法即可
你还可以在组件内的beforeRouteLeave
钩子函数里面执行移除某些组件的逻辑
- 使用上述方法需要注意的事项是
- 给缓存组件添加组件名称;
- 需要在路由记录里面配置好
compName
选项,并且组织好你的level
,因为在实际业务比demo
复杂很多; - 缓存组件会激活
activated
钩子,你需要在该函数里面执行添加缓存的方法,不然整个清缓存是不起作用的; - 默认的清除规则是移除所有低层级的缓存组件(即缓存组件列表1、2、3,从列表3跳到列表1,列表2、3均会清除);
- 边界情况的也会清除(即如果列表2、3 的
level
相同,从列表3跳到列表2,会清除列表3的缓存);
- 你可能注意到了一个问题,在整个项目中配置不支持动态修改的,即在整个项目中缓存移除的规则是不同时支持两种模式的,不想麻烦做是因为
vue
混入的缘故,全局的beforeRouteLeave
会在组件内beforeRouteLeave
之前执行,所以你懂得...不过你无需担心有死角的清除问题,因为你可以通过mc_removeCachedByComponentName
该方法来清除任意你想要销毁的组件。
2019/05/04 - 新增对 TS 支持
- 如果你是
vue + ts
的开发方式,可以采用下面的方式,由于当前vue
(或者 <2.6.10)的版本对ts
支持不是很好,所以大部分是采用vue-shims.d.ts
的方式来进行模块拓展,更多的使用细节可参考 vue 官网对 Typescript 的支持 以及 Typescript 模块拓展 -
下面是文件相对位置关系
- vue-shims.d.ts 文件内容
/*
* @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 = {}
private mc_cachedComponentsInfo: 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 当前组件实例
*/
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
}
}
/**
* 根据组件名称移除指定的组件
* @param componentName 要移除的组件名称或者名称列表
*/
mc_removeCachedByComponentName (componentName: string | string[]) {
if (!isArray(componentName) && typeof componentName !== 'string') {
throw new TypeError(`移除的组件可以是 array 或者 string,当前类型为: ${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} 组件非缓存组件,请添加正确的缓存组件名`)
}
}
/**
* 移除路由级别的缓存组件
* @param toRoute 跳转路由记录
* @param Vue 当前组件实例
*/
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
会更加方便,当然更希望尤大能够增加缓存操作的api
,这样就不再为了缓存而造各种轮子了。
写在最后
- 这篇文章主要参考的是 vue组件缓存源码,感兴趣的可以看一下;
- 本文为原创文章,如果需要转载,请注明出处,方便溯源,如有错误地方,可以在下方留言,欢迎斧正,
demo
已经上传到 关于vue缓存清除的个人git仓库