keepAlive的最大缓存数是无限大
当我们未设置keepAlive的最大缓存数时,当缓存组件太多,会导致内存溢出。
keepAlive最大缓存数测试实践
- 如下脚本,给组件设置keepAlive缓存,并将组件的key设置为
$route.path
js
<router-view v-slot="{ Component }">
<keep-alive >
<component :is="Component" :key="$route.path"/>
</keep-alive>
</router-view>
- 如下脚本,设置一个动态路由,不同id共用同一个组件
js
{
path: '/el/cascader/:id',
name: 'ElCascaderDemo2',
component: () => import('../views/ElCascaderDemo.vue')
},
- 如下脚本, 点击每一行,可以不断的新增缓存组件
js
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
const go = (id: number) => {
router.push({
path: '/el/cascader/' + id
})
}
</script>
<template>
<main>
<div v-for="i in 10000" @click="go(i)" class="item">
{{ i }}
</div>
</main>
</template>
- 下面脚本是keepAlive组件中的源码,在内部打断点,查看当前cache中缓存的组件数是多少。
js
const cacheSubtree = () => {
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree));
}
};
onMounted(cacheSubtree);
onUpdated(cacheSubtree);
-
如下图,当cache中缓存12个组件时,内存已经达到了2G,
-
如下图,在继续加压后,内存达到4019MB时,页面崩溃。

故在使用keepAlive缓存组件,一定要设置它的最大缓存数。
设置keepAlive最大缓存数
js
<router-view v-slot="{ Component }">
<keep-alive :max="10">
<component :is="Component" :key="$route.path"/>
</keep-alive>
</router-view>
设置最大缓存数等于10以后,keepAlive组件内缓存变量cache的size<=9,内存位置在1900MB。
实现手动删除缓存组件
实现手动删除缓存组件的方式是:动态增删keepAlive组件的exclude属性。exclude属性的值可以是一个组件名称组成的数组。在 3.2.34 或以上的版本中,使用 <script setup>
的单文件组件会自动根据文件名生成对应的 name
选项,无需再手动声明。但目前组件的key等于route.path
,/el/cascader/1
和/el/cascader/2
会缓存两份,手动删除这类组件,需要给它们各自一个名称,而不是都用它们指向的那一个组件的名称。
第一步:封装动态组件component的is属性的赋值,使用route.path作为组件的名称。
js
<router-view v-slot="{ Component }">
<keep-alive :max="10">
<component :is="formatComponentInstance(Component, $route?.path)" :key="$route.path"/>
</keep-alive>
</router-view>
let wrapperMap = new Map()
const formatComponentInstance = (component : Component, path:string ) => {
let wrapper
if (wrapperMap.has(path)) {
wrapper = wrapperMap.get(path)
} else {
wrapper = {
name: path,
render(){
return h(component) // h的第一个参数可以是字符串,也可以是一个组件定义;h返回的是一个虚拟dom
}
}
wrapperMap.set(path, wrapper)
}
return h(wrapper)
}
第二步:实现一个简单内页签,内页签关闭时清除组件缓存
js
<template>
<el-tag
v-for="item in editableTabs"
:key="item.key"
closable
:disable-transitions="false"
@click="tabChange(item.key)"
@close="handleTabsEdit(item.key, 'remove', undefined)"
:type="editableKey === item.key ? 'primary':'info'"
>
{{ item.title }}
</el-tag>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import type { Ref } from 'vue'
import { ElTag} from 'element-plus'
import { useRoute, useRouter} from 'vue-router'
const route = useRoute()
const router = useRouter()
import { useKeepAlive } from '../stores/index'
let tabIndex = 1
const editableKey = ref('')
const editableTabs : Ref<Array<{[key: string]: any}>> = ref([])
const visitedRoute: String [] = []
watch(() => route.path, (val) => {
if (visitedRoute.indexOf(val) === -1) {
visitedRoute.push(val)
handleTabsEdit(undefined, 'add', val)
} else {
editableKey.value = editableTabs.value.filter((tab) => tab.title === val)[0].key
}
})
const handleTabsEdit = (
targetKey: string | undefined,
action: 'remove' | 'add',
newTabTitle: string | undefined
) => {
if (action === 'add') {
const newTabName = `${++tabIndex}`
editableTabs.value.push({
title: newTabTitle,
key: newTabName,
})
editableKey.value = newTabName
} else if (action === 'remove') {
const tabs = editableTabs.value
let activeKey = editableKey.value
if (activeKey === targetKey) {
tabs.forEach((tab, index) => {
if (tab.key === targetKey) {
// 同步删除visitedRoute数组
let includeIndex = visitedRoute.indexOf(tab.title)
visitedRoute.splice(includeIndex, 1)
// 重置active tab
const nextTab = tabs[index + 1] || tabs[index - 1]
if (nextTab) {
activeKey = nextTab.name
router.push({
path: nextTab.title
})
}
// /页签关闭时,重置store中的exclude数组,如['/el/cascader/2']
const keepAliveStore = useKeepAlive()
let exclude: string[] = [tab.title]
keepAliveStore.setExclude(exclude)
}
})
}
editableKey.value = activeKey
editableTabs.value = tabs.filter((tab) => tab.key !== targetKey)
}
}
const tabChange = (activeKey: string) => {
let path = editableTabs.value.filter((tab) => tab.key === activeKey)[0].title
router.push({
path
})
}
</script>
js
const keepAliveStore = useKeepAlive()
const excludes = computed(() => {
return keepAliveStore.exclude
})
<router-view v-slot="{ Component }">
<keep-alive :max="10" :exclude="excludes">
<component :is="formatComponentInstance(Component, $route?.path)" :key="$route.path"/>
</keep-alive>
</router-view>
在调试中出现以下报错:
Uncaught (in promise) TypeError: parentComponent.ctx.deactivate is not a function
报错的原因是因为我将上面的封装简化为以下脚本, 导致同一个path两次产生的组件vnode的type不一样。为什么要简化呢, 因为考虑到wrapperMap会有一定的内存消耗。
js
const formatComponentInstance = (component : Component, path:string ) => {
let wrapper = {
name: path,
render(){
return h(component) // h的第一个参数可以是字符串,也可以是一个组件定义;h返回的是一个虚拟dom
}
}
return h(wrapper)
}
在vue源码中的isSameVNodeType
中的n1.type === n2.type
的判断中, 组件转化的vnode的type是一个对象,对于相同的route.path, 每次通过上面的封装脚本产生新的组件实例,会导致每次产生的vnode的type对象不是同一个对象,导致n1.type不等于n2.type, isSameVNodeType返回false,会卸载n1,卸载时就产生了上面的报错。
js
function isSameVNodeType(n1, n2) {
if (n2.shapeFlag & 6 && hmrDirtyComponents.has(n2.type)) {
n1.shapeFlag &= ~256;
n2.shapeFlag &= ~512;
return false;
}
return n1.type === n2.type && n1.key === n2.key;
}