Appearance
第6.1节:KeepAlive:组件缓存与状态保持的策略
引言
KeepAlive 是 Vue.js 中一个特殊的内置组件,用于缓存组件实例并保持其状态。它通过智能的缓存策略和生命周期管理,避免了组件的重复创建和销毁,从而提升应用性能。本节将深入分析 KeepAlive 的实现原理,包括缓存机制、LRU 算法、生命周期处理等核心特性。
6.1.1 KeepAlive 核心概念
基本用法
KeepAlive 组件通过包裹动态组件来实现缓存:
vue
<template>
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
</template>核心属性
KeepAlive 支持三个核心属性:
typescript
export interface KeepAliveProps {
include?: MatchPattern // 只有名称匹配的组件会被缓存
exclude?: MatchPattern // 名称匹配的组件不会被缓存
max?: number | string // 最多可以缓存多少组件实例
}
type MatchPattern = string | RegExp | (string | RegExp)[]类型定义
typescript
type CacheKey = PropertyKey | ConcreteComponent
type Cache = Map<CacheKey, VNode>
type Keys = Set<CacheKey>
export interface KeepAliveContext extends ComponentRenderContext {
renderer: RendererInternals
activate: (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
namespace: ElementNamespace,
optimized: boolean,
) => void
deactivate: (vnode: VNode) => void
}6.1.2 组件缓存机制
缓存数据结构
KeepAlive 使用 Map 和 Set 来管理缓存:
typescript
const cache: Cache = new Map() // 存储缓存的 VNode
const keys: Keys = new Set() // 维护缓存键的访问顺序(LRU)
let current: VNode | null = null // 当前活跃的组件缓存键生成
缓存键的生成逻辑确保了组件的唯一性:
typescript
// 在渲染函数中
const key = vnode.key == null ? comp : vnode.key
const cachedVNode = cache.get(key)缓存键的优先级:
- 如果 VNode 有显式的
key属性,使用该 key - 否则使用组件的构造函数作为 key
缓存命中处理
当缓存命中时,KeepAlive 会复用已缓存的组件实例:
typescript
if (cachedVNode) {
// 复制已挂载的状态
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
// 处理过渡动画
if (vnode.transition) {
setTransitionHooks(vnode, vnode.transition!)
}
// 避免 vnode 被当作新组件挂载
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// 更新 LRU 顺序:将当前 key 移到最新位置
keys.delete(key)
keys.add(key)
} else {
// 缓存未命中,添加新的缓存项
keys.add(key)
// 检查是否超出最大缓存数量
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value!)
}
}
// 标记组件应该被缓存
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE缓存存储时机
KeepAlive 在组件挂载和更新后缓存子树:
typescript
let pendingCacheKey: CacheKey | null = null
const cacheSubtree = () => {
if (pendingCacheKey != null) {
// 如果 KeepAlive 的子组件是 Suspense,需要等 Suspense 解析后再缓存
if (isSuspense(instance.subTree.type)) {
queuePostRenderEffect(() => {
cache.set(pendingCacheKey!, getInnerChild(instance.subTree))
}, instance.subTree.suspense)
} else {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
}
onMounted(cacheSubtree)
onUpdated(cacheSubtree)6.1.3 LRU 算法实现
LRU 原理
KeepAlive 使用 LRU(Least Recently Used)算法来管理缓存,当缓存达到最大数量时,会移除最久未使用的组件。
LRU 数据结构
typescript
// 使用 Set 来维护访问顺序
const keys: Keys = new Set<CacheKey>()
// Set 的特性:
// 1. 插入顺序保持不变
// 2. delete + add 可以将元素移到末尾
// 3. keys.values().next().value 获取最旧的元素LRU 更新逻辑
typescript
// 缓存命中时,更新 LRU 顺序
if (cachedVNode) {
// 删除旧位置
keys.delete(key)
// 添加到新位置(末尾)
keys.add(key)
} else {
// 新增缓存项
keys.add(key)
// 检查是否超出限制
if (max && keys.size > parseInt(max as string, 10)) {
// 移除最久未使用的项(Set 的第一个元素)
pruneCacheEntry(keys.values().next().value!)
}
}缓存清理机制
typescript
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key) as VNode
// 如果缓存的组件不是当前活跃组件,则卸载它
if (cached && (!current || !isSameVNodeType(cached, current))) {
unmount(cached)
} else if (current) {
// 当前活跃实例不应该继续被缓存
// 现在不能卸载它,但可能稍后会卸载,所以重置其标志
resetShapeFlag(current)
}
// 从缓存中移除
cache.delete(key)
keys.delete(key)
}
function unmount(vnode: VNode) {
// 重置 shapeFlag 以便正确卸载
resetShapeFlag(vnode)
_unmount(vnode, instance, parentSuspense, true)
}6.1.4 生命周期管理
activated 和 deactivated 钩子
KeepAlive 引入了两个特殊的生命周期钩子:
typescript
export function onActivated(
hook: Function,
target?: ComponentInternalInstance | null,
): void {
registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
}
export function onDeactivated(
hook: Function,
target?: ComponentInternalInstance | null,
): void {
registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
}生命周期钩子注册
typescript
function registerKeepAliveHook(
hook: Function & { __wdc?: Function },
type: LifecycleHooks,
target: ComponentInternalInstance | null = currentInstance,
) {
// 缓存去激活分支检查包装器,确保相同的钩子可以被调度器正确去重
const wrappedHook =
hook.__wdc ||
(hook.__wdc = () => {
// 只有当目标实例不在去激活分支中时才触发钩子
let current: ComponentInternalInstance | null = target
while (current) {
if (current.isDeactivated) {
return
}
current = current.parent
}
return hook()
})
injectHook(type, wrappedHook, target)
// 除了在目标实例上注册,还要向上遍历父链
// 在所有作为 keep-alive 根的祖先实例上注册
if (target) {
let current = target.parent
while (current && current.parent) {
if (isKeepAlive(current.parent.vnode)) {
injectToKeepAliveRoot(wrappedHook, type, target, current)
}
current = current.parent
}
}
}激活和去激活处理
typescript
// 激活组件
sharedContext.activate = (
vnode,
container,
anchor,
namespace,
optimized,
) => {
const instance = vnode.component!
// 移动 DOM 节点到目标容器
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// 更新 props(可能已更改)
patch(
instance.vnode,
vnode,
container,
anchor,
instance,
parentSuspense,
namespace,
vnode.slotScopeIds,
optimized,
)
// 异步执行激活钩子
queuePostRenderEffect(() => {
instance.isDeactivated = false
if (instance.a) {
invokeArrayFns(instance.a) // 调用 activated 钩子
}
const vnodeHook = vnode.props && vnode.props.onVnodeMounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
}, parentSuspense)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentAdded(instance)
}
}
// 去激活组件
sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
// 取消挂载和激活的副作用
invalidateMount(instance.m)
invalidateMount(instance.a)
// 移动 DOM 节点到存储容器
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
// 异步执行去激活钩子
queuePostRenderEffect(() => {
if (instance.da) {
invokeArrayFns(instance.da) // 调用 deactivated 钩子
}
const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
instance.isDeactivated = true
}, parentSuspense)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentAdded(instance)
}
}6.1.5 include/exclude 过滤机制
模式匹配函数
typescript
function matches(pattern: MatchPattern, name: string): boolean {
if (isArray(pattern)) {
return pattern.some((p: string | RegExp) => matches(p, name))
} else if (isString(pattern)) {
return pattern.split(',').includes(name)
} else if (isRegExp(pattern)) {
pattern.lastIndex = 0
return pattern.test(name)
}
return false
}过滤逻辑
typescript
// 在渲染函数中进行过滤
const name = getComponentName(
isAsyncWrapper(vnode)
? (vnode.type as ComponentOptions).__asyncResolved || {}
: comp,
)
const { include, exclude, max } = props
// 检查是否应该缓存
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
// 不缓存,移除 SHOULD_KEEP_ALIVE 标志
vnode.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode
return rawVNode
}动态过滤更新
typescript
// 监听 include/exclude 属性变化
watch(
() => [props.include, props.exclude],
([include, exclude]) => {
include && pruneCache(name => matches(include, name))
exclude && pruneCache(name => !matches(exclude, name))
},
// 在渲染后修剪,确保 current 已更新
{ flush: 'post', deep: true },
)
function pruneCache(filter: (name: string) => boolean) {
cache.forEach((vnode, key) => {
const name = getComponentName(vnode.type as ConcreteComponent)
if (name && !filter(name)) {
pruneCacheEntry(key)
}
})
}6.1.6 DOM 节点管理
存储容器
KeepAlive 创建一个隐藏的存储容器来保存去激活的组件:
typescript
const storageContainer = createElement('div')DOM 移动策略
typescript
// 激活时:从存储容器移动到目标容器
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// 去激活时:从目标容器移动到存储容器
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)ShapeFlag 管理
typescript
function resetShapeFlag(vnode: VNode) {
// 使用位运算移除 keep-alive 标志
vnode.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
vnode.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE
}
// 标记组件应该被缓存
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
// 标记组件已被缓存
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE6.1.7 内存管理与性能优化
内存泄漏防护
typescript
// 组件卸载时清理所有缓存
onBeforeUnmount(() => {
cache.forEach(cached => {
const { subTree, suspense } = instance
const vnode = getInnerChild(subTree)
if (cached.type === vnode.type && cached.key === vnode.key) {
// 当前实例将作为 keep-alive 卸载的一部分被卸载
resetShapeFlag(vnode)
// 但在这里调用其 deactivated 钩子
const da = vnode.component!.da
da && queuePostRenderEffect(da, suspense)
return
}
unmount(cached)
})
})缓存大小控制
typescript
// 通过 max 属性控制最大缓存数量
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value!)
}异步组件处理
typescript
// 获取内部子组件(处理 Suspense 包装)
function getInnerChild(vnode: VNode) {
return vnode.shapeFlag & ShapeFlags.SUSPENSE ? vnode.ssContent! : vnode
}
// 处理异步组件的名称获取
const name = getComponentName(
isAsyncWrapper(vnode)
? (vnode.type as ComponentOptions).__asyncResolved || {}
: comp,
)6.1.8 实际应用场景
路由缓存
vue
<template>
<router-view v-slot="{ Component }">
<KeepAlive :include="cachedViews">
<component :is="Component" :key="$route.fullPath" />
</KeepAlive>
</router-view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const cachedViews = ref(['Home', 'Profile', 'Settings'])
// 动态控制缓存
const shouldCache = computed(() => {
return route.meta?.keepAlive !== false
})
</script>标签页缓存
vue
<template>
<div class="tabs">
<div class="tab-headers">
<button
v-for="tab in tabs"
:key="tab.id"
:class="{ active: currentTab === tab.id }"
@click="currentTab = tab.id"
>
{{ tab.title }}
<span @click.stop="closeTab(tab.id)">×</span>
</button>
</div>
<KeepAlive :include="cachedTabs">
<component
:is="currentTabComponent"
:key="currentTab"
/>
</KeepAlive>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const tabs = ref([
{ id: 'tab1', title: 'Tab 1', component: 'TabContent1' },
{ id: 'tab2', title: 'Tab 2', component: 'TabContent2' },
{ id: 'tab3', title: 'Tab 3', component: 'TabContent3' },
])
const currentTab = ref('tab1')
const cachedTabs = ref(['TabContent1', 'TabContent2'])
const currentTabComponent = computed(() => {
const tab = tabs.value.find(t => t.id === currentTab.value)
return tab?.component
})
function closeTab(tabId) {
const index = tabs.value.findIndex(t => t.id === tabId)
if (index > -1) {
tabs.value.splice(index, 1)
// 从缓存中移除
const component = tabs.value[index]?.component
if (component) {
const cacheIndex = cachedTabs.value.indexOf(component)
if (cacheIndex > -1) {
cachedTabs.value.splice(cacheIndex, 1)
}
}
}
}
</script>表单状态保持
vue
<template>
<KeepAlive>
<FormComponent
v-if="showForm"
@submit="handleSubmit"
@cancel="showForm = false"
/>
</KeepAlive>
</template>
<script setup>
import { ref, onActivated, onDeactivated } from 'vue'
const showForm = ref(true)
// 在组件内部使用生命周期钩子
const FormComponent = {
setup() {
const formData = ref({
name: '',
email: '',
message: ''
})
onActivated(() => {
console.log('表单组件被激活')
// 可以在这里恢复焦点、刷新数据等
})
onDeactivated(() => {
console.log('表单组件被去激活')
// 可以在这里保存草稿、清理定时器等
})
return { formData }
}
}
</script>6.1.9 性能优化策略
缓存策略优化
typescript
// 1. 合理设置 max 值
<KeepAlive :max="10">
<component :is="currentComponent" />
</KeepAlive>
// 2. 精确控制 include/exclude
<KeepAlive :include="/^(Home|Profile)$/">
<router-view />
</KeepAlive>
// 3. 动态调整缓存策略
const cacheStrategy = computed(() => {
if (isLowMemoryDevice()) {
return { max: 3, include: ['Home'] }
}
return { max: 10, include: cachedComponents.value }
})内存监控
typescript
// 监控缓存使用情况
function useCacheMonitor() {
const cacheSize = ref(0)
const memoryUsage = ref(0)
const updateStats = () => {
// 获取缓存大小
const keepAliveInstance = getCurrentInstance()
if (keepAliveInstance?.__v_cache) {
cacheSize.value = keepAliveInstance.__v_cache.size
}
// 监控内存使用(仅在支持的浏览器中)
if (performance.memory) {
memoryUsage.value = performance.memory.usedJSHeapSize
}
}
onMounted(() => {
const timer = setInterval(updateStats, 5000)
onUnmounted(() => clearInterval(timer))
})
return { cacheSize, memoryUsage }
}预加载策略
typescript
// 预加载关键组件
function usePreloadStrategy() {
const preloadComponent = async (componentName: string) => {
try {
const component = await import(`@/components/${componentName}.vue`)
// 预创建组件实例但不挂载
return component.default
} catch (error) {
console.warn(`Failed to preload component: ${componentName}`, error)
}
}
onMounted(() => {
// 预加载常用组件
const commonComponents = ['UserProfile', 'Settings', 'Dashboard']
commonComponents.forEach(preloadComponent)
})
}6.1.10 调试与开发工具
开发时调试
typescript
// 开发环境下的缓存状态监控
if (__DEV__) {
const instance = getCurrentInstance()
if (instance) {
// 暴露缓存实例供调试
;(instance as any).__v_cache = cache
// 添加调试信息
window.__VUE_KEEPALIVE_CACHE__ = {
cache,
keys,
current,
stats: {
size: () => cache.size,
keys: () => Array.from(keys),
clear: () => {
cache.clear()
keys.clear()
}
}
}
}
}DevTools 集成
typescript
// DevTools 组件树更新
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentAdded(instance)
}总结
KeepAlive 组件通过精心设计的缓存机制和生命周期管理,为 Vue.js 应用提供了强大的组件状态保持能力。其核心特性包括:
关键特性
- 智能缓存:基于 Map 和 Set 的高效缓存数据结构
- LRU 算法:自动管理缓存大小,移除最久未使用的组件
- 生命周期扩展:提供 activated/deactivated 钩子
- 灵活过滤:支持 include/exclude 模式匹配
- DOM 优化:通过 DOM 移动而非重新创建来提升性能
性能优势
- 避免重复渲染:缓存组件实例和 DOM 状态
- 保持用户状态:表单数据、滚动位置等状态得以保留
- 减少网络请求:避免重复的数据获取
- 优化用户体验:快速的页面切换和状态恢复
最佳实践
- 合理设置缓存大小:根据应用需求和设备性能调整 max 值
- 精确控制缓存范围:使用 include/exclude 避免不必要的缓存
- 监控内存使用:在生产环境中监控缓存对内存的影响
- 处理副作用:在 activated/deactivated 钩子中正确处理定时器、事件监听器等
KeepAlive 的实现展现了 Vue.js 在性能优化方面的深度思考,为开发者提供了一个既强大又易用的组件缓存解决方案。
