Appearance
KeepAlive:组件缓存与状态保持的策略
引言
在 Vue 中,使用动态组件(<component :is="view">)或 v-if 切换组件时,Vue 的默认行为是**卸载(unmount)旧组件,并挂载(mount)**新组件。这个过程会销毁旧组件的实例,导致其内部状态(如表单数据、滚动位置)全部丢失。
<KeepAlive> 是一个内置组件,它的主要作用是解决这个问题。它会“捕获”本应被卸载的组件 VNode,将其“缓存”在内存中,并在下次需要时“激活”它,而不是重新创建。
核心:setup 函数与缓存系统
<KeepAlive> 的所有逻辑都定义在它的 setup 函数中 (packages/runtime-core/src/components/KeepAlive.ts)。它通过 setup 创建并管理一个缓存系统,并返回一个自定义的 render 函数来控制渲染流程。
typescript
// core/packages/runtime-core/src/components/KeepAlive.ts
setup(props: KeepAliveProps, { slots }: SetupContext) {
const instance = getCurrentInstance()!
// 1. 【缓存核心】
// cache: Map<CacheKey, VNode>
// - 键 (key): 组件的 key 或 type
// - 值 (value): 缓存的 VNode 实例
const cache: Cache = new Map()
// keys: Set<CacheKey>
// - 用于 LRU 算法,存储键的“访问顺序”
const keys: Keys = new Set()
// 2. 【DOM 存储】
// 创建一个隐藏的 <div>,用作“存储容器”
// 所有“去激活”的组件 DOM 都会被移动到这里
const storageContainer = createElement('div')
// 3. 【渲染器接口】
// 获取底层的渲染器方法
const {
renderer: {
p: patch, // patch
m: move, // move (DOM 移动)
um: _unmount, // unmount
o: { createElement },
},
} = (instance.ctx as KeepAliveContext).renderer
// 4. 【定义“激活”与“去激活”动作】
// “去激活”:把组件移入“存储容器”
function deactivate(vnode: VNode) {
// 移动 DOM 节点到 storageContainer
move(vnode, storageContainer, null, MoveType.LEAVE)
// 异步执行 onDeactivated 钩子
queuePostRenderEffect(() => {
instance.da && invokeArrayFns(instance.da) // 'da' -> onDeactivated
instance.isDeactivated = true
}, parentSuspense)
}
// “激活”:把组件移出“存储容器”,放回页面
function activate(vnode, container, anchor) {
// 移动 DOM 节点回 container
move(vnode, container, anchor, MoveType.ENTER)
// 重新 patch (更新 props)
patch(instance.vnode, vnode, container, anchor, ...)
// 异步执行 onActivated 钩子
queuePostRenderEffect(() => {
instance.isDeactivated = false
if (instance.a) invokeArrayFns(instance.a) // 'a' -> onActivated
}, parentSuspense)
}
// ... (pruneCacheEntry, onBeforeUnmount 等) ...
// 5. 【返回自定义 Render 函数】
// 这个函数是 KeepAlive 的渲染逻辑核心
return () => {
// ... (渲染逻辑) ...
}
}自定义 render:渲染决策
<KeepAlive> setup 返回的自定义 render 函数是整个系统的控制中心。每当 <KeepAlive> 需要重新渲染时,这个函数就会运行。
它的核心逻辑如下:
typescript
// KeepAlive 的 setup 返回的 render 函数
return () => {
// 1. 【获取子组件】
// 获取 <KeepAlive> 插槽中的“当前”子组件 VNode
const vnode = slots.default && slots.default()[0]
if (vnode == null) return null // 没有子组件
const comp = vnode.type as ConcreteComponent
// 2. 【过滤】
// 检查 props: include / exclude
const name = getComponentName(comp)
const { include, exclude, max } = props
if (
(include && (!name || !matches(include, name))) || // 不在 include 中
(exclude && name && matches(exclude, name)) // 在 exclude 中
) {
// 不缓存,将其标记为 current,并直接渲染
current = vnode
return vnode // 返回 VNode,让渲染器按正常流程处理
}
// 3. 【生成缓存键】
// 优先用 VNode 上的 key,否则用组件类型 (comp)
const key = vnode.key == null ? comp : vnode.key
// 4. 【查找缓存】
const cachedVNode = cache.get(key)
// --------------------------------------------------
// ▼ 关键的渲染决策 ▼
// --------------------------------------------------
if (cachedVNode) {
// 5. 【路径 A:缓存命中 -> 激活】
// a. 复制实例和 DOM
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
// b. 标记 VNode:
// 告诉渲染器,这是一个“被激活”的组件,
// 不要走“挂载(mount)”流程,而是走“激活(activate)”流程
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// c. 【LRU 优化】
// 将 key 移动到 Set 的末尾 (标记为“最近使用”)
keys.delete(key)
keys.add(key)
} else {
// 6. 【路径 B:缓存未命中 -> 缓存】
// a. 添加到 LRU 队列
keys.add(key)
// b. 【LRU 优化】
// 检查是否超出 max 限制
if (max && keys.size > parseInt(max as string, 10)) {
// 移除“最久未使用”的 key (Set 的第一个元素)
pruneCacheEntry(keys.values().next().value)
}
// c. 标记 VNode:
// 告诉渲染器,这个组件在“挂载”后,
// 应该被“缓存”,而不是“销毁”
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
}
// 7. 【交换】
// 无论如何,都要“去激活”上一个组件
if (current && current.key !== vnode.key) {
deactivate(current)
}
// 将当前 VNode 设为“活跃”
current = vnode
// 8. 返回 VNode,让渲染器继续处理
return vnode
}ShapeFlags 的作用:
COMPONENT_SHOULD_KEEP_ALIVE:告诉渲染器,在patch之后,调用<KeepAlive>的onMounted/onUpdated钩子来保存这个 VNode 到cache中。COMPONENT_KEPT_ALIVE:告诉渲染器,在patch之前,跳过mount,改为调用<KeepAlive>的activate函数(激活)。
缓存清理:pruneCacheEntry
当缓存数量超出 max 限制时,pruneCacheEntry(修剪缓存条目)会被调用,它负责销毁并移除最久未使用的组件。
typescript
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key) as VNode
// 1. 如果这个 VNode 不是当前活跃的,就卸载它
if (!current || !isSameVNodeType(cached, current)) {
_unmount(cached, instance, parentSuspense, true)
}
// 2. 从缓存中删除
cache.delete(key)
keys.delete(key)
}LRU (Least Recently Used) 算法的实现: <KeepAlive> 使用 Set 来实现 LRU:
keys: Set会保持插入顺序。keys.values().next().value永远是最先插入的(即最久未使用的)key。keys.delete(key)+keys.add(key):这个“先删后加”的操作,会把key移动到Set的末尾,使其成为“最近使用的”。
组件的生命周期:onActivated / onDeactivated
<KeepAlive> 的子组件可以通过 onActivated 和 onDeactivated 钩子,感知自己何时被“激活”或“去激活”。
- 在
setup中,deactivate(vnode)和activate(vnode)函数会分别调用instance.da(deactivated 钩子数组) 和instance.a(activated 钩子数组)。 - 这两个钩子只在
<KeepAlive>的子组件中才会被注册和执行。
应用场景:
javascript
import { onActivated, onDeactivated, ref } from 'vue'
setup() {
const timer = ref(null)
onActivated(() => {
// 组件被“激活”(从缓存移回 DOM)时
console.log('组件被激活')
// 重新开启定时器
timer.value = setInterval(...)
})
onDeactivated(() => {
// 组件被“去激活”(从 DOM 移入缓存)时
console.log('组件被去激活')
// 清理定时器,防止内存泄漏
clearInterval(timer.value)
})
}动态缓存策略:include / exclude
props.include 和 props.exclude 提供了对缓存组件的精确控制。
matches函数: 在render函数中,matches函数会检查组件name是否匹配include/exclude规则(支持字符串、正则、数组)。typescriptfunction 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)) { return pattern.test(name) } return false }watch动态清理:<KeepAlive>内部有一个watch来侦听include/excludeprop 的变化。如果规则改变了,watch会立即遍历现有缓存,将不再符合规则的组件清理掉(调用pruneCacheEntry)。typescriptwatch( () => [props.include, props.exclude], ([include, exclude]) => { // 遍历 cache,prune 掉不符合新规则的 include && pruneCache(name => matches(include, name)) exclude && pruneCache(name => !matches(exclude, name)) }, { flush: 'post', deep: true }, )
总结
<KeepAlive> 是 Vue 渲染器的一个特殊组件。
- 它通过自定义
setup和render函数来接管子组件的渲染。 - 它使用
Map存储 VNode,使用Set实现高效的 LRU 策略来管理缓存。 - 它通过
ShapeFlags标记,拦截渲染器的mount(挂载)和unmount(卸载)默认行为。 - 它调用底层的
move(移动)指令,将组件 DOM 在“渲染容器”和“隐藏的存储容器”之间切换。 - 它在
move的同时,通过onActivated和onDeactivated钩子通知子组件其状态的变更。 - 它通过
watch监控include/exclude,动态地清理缓存,并通过onBeforeUnmount防止内存泄漏。
