1. 组件示例
让我们先看一个典型的 KeepAlive 使用场景:
// 1. 定义两个组件
// TabA.vue
<template>
<div>
<span>Count: {{ count }}</span>
<button @click="count++">Add</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
// TabB.vue
<template>
<input v-model="message" />
</template>
<script setup>
import { ref } from 'vue'
const message = ref('')
</script>
// App.vue
<template>
<div>
<div>
<button @click="currentTab = 'TabA'">Tab A</button>
<button @click="currentTab = 'TabB'">Tab B</button>
</div>
<keep-alive :max="2" :include="['TabA', 'TabB']">
<component :is="currentTab === 'TabA' ? TabA : TabB" />
</keep-alive>
</div>
</template>
<script setup>
import { ref } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'
const currentTab = ref('TabA')
</script>
这个示例展示了:
-
两个需要缓存的组件:TabA(计数器)和 TabB(输入框)
-
KeepAlive 的基本配置:
- max: 最大缓存数量
- include: 需要缓存的组件名
-
使用 component 动态组件实现切换
-
组件状态(count 和 message)在切换时会被保持
2. 组件数据结构
KeepAlive 组件的核心数据结构:
// 匹配模式类型:字符串、正则或它们的数组
type MatchPattern = string | RegExp | (string | RegExp)[]
// 缓存键类型
type CacheKey = PropertyKey | ConcreteComponent;
// 缓存存储
type Cache = Map<CacheKey, VNode>;
// 缓存键集合
type Keys = Set<CacheKey>;
// 组件属性
interface KeepAliveProps {
include?: MatchPattern; // 包含的组件
exclude?: MatchPattern; // 排除的组件
max?: number | string; // 最大缓存数
}
// KeepAlive 上下文接口
interface KeepAliveContext extends ComponentRenderContext {
renderer: RendererInternals;
activate: (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
namespace: ElementNamespace,
optimized: boolean
) => void;
deactivate: (vnode: VNode) => void;
}
// 组件定义
const KeepAliveImpl: ComponentOptions = {
name: 'KeepAlive',
// 用于渲染器中特殊处理的标记
__isKeepAlive: true,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
}
}
// 导出的公共类型
export const KeepAlive = {
__isKeepAlive: true,
new(): {
$props: VNodeProps & KeepAliveProps;
$slots: {
default(): VNode[];
};
};
}
关键数据结构说明:
-
匹配相关:
- MatchPattern:用于 include/exclude 的匹配模式
- matches 函数:支持字符串、正则和数组的匹配
-
缓存相关:
- Cache:使用 Map 存储组件 VNode
- Keys:使用 Set 实现 LRU 缓存策略
- CacheKey:支持组件类型或自定义 key
-
组件通信:
- KeepAliveContext:与渲染器通信的接口
- activate/deactivate:组件激活和停用的方法
-
类型标记:
- __isKeepAlive:用于渲染器识别 KeepAlive 组件
- ShapeFlags:用于标记组件的缓存状态
3. 实现原理分析
3.1 初始化阶段
当 KeepAlive 组件被创建时,setup 函数首先进行初始化:
setup(props: KeepAliveProps, { slots }: SetupContext) {
const instance = getCurrentInstance()!
const sharedContext = instance.ctx as KeepAliveContext
// 1. 创建缓存存储
const cache: Cache = new Map()
const keys: Keys = new Set()
let current: VNode | null = null
let pendingCacheKey: CacheKey | null = null
// 2. 获取渲染器方法
const {
renderer: {
p: patch,
m: move,
um: _unmount,
o: { createElement }
}
} = sharedContext
// 3. 创建隐藏容器
const storageContainer = createElement('div')
// 4. 定义卸载方法
const unmount = (vnode: VNode) => {
// 重置 keep alive 标记
resetShapeFlag(vnode)
_unmount(vnode, instance, parentSuspense, true)
}
// 5. 定义缓存清理方法
function pruneCache(filter?: (name: string) => boolean) {
cache.forEach((vnode, key) => {
const name = getComponentName(vnode.type as ConcreteComponent)
if (name && (!filter || !filter(name))) {
pruneCacheEntry(key)
}
})
}
// 6. 处理 Suspense 组件
if (__SSR__ && !sharedContext.renderer) {
return () => {
const children = slots.default && slots.default()
return children && children.length === 1 ? children[0] : children
}
}
// 7. 注册缓存更新钩子
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
// 8. 注册卸载钩子
onBeforeUnmount(() => {
cache.forEach(cached => {
unmount(cached)
})
})
// 9. 监听 include/exclude 变化
watch(
() => [props.include, props.exclude],
([include, exclude]) => {
include && pruneCache(name => matches(include, name))
exclude && pruneCache(name => !matches(exclude, name))
},
{ flush: 'post', deep: true }
)
// ... 后续实现
}
setup 函数的主要职责:
- 初始化缓存相关的数据结构和状态
- 获取渲染器方法,创建存储容器
- 定义组件缓存和清理的核心方法
- 处理服务端渲染的特殊情况
- 注册生命周期钩子和监听器
- 返回渲染函数
3.2 组件首次渲染
以上面的示例为例,当首次渲染 TabA 时:
return () => {
// 重置缓存键
pendingCacheKey = null;
// 1. 获取要渲染的组件
if (!slots.default) {
return null;
}
const children = slots.default();
const rawVNode = children[0];
// 检查是否只有一个子组件
if (children.length > 1) {
if (__DEV__) {
warn(`KeepAlive should contain exactly one component child.`);
}
current = null;
return children;
}
// 检查是否是有效的组件
if (
!isVNode(rawVNode) ||
(!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
!(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
) {
current = null;
return rawVNode;
}
// 2. 获取组件信息
let vnode = getInnerChild(rawVNode);
// 处理 Comment 类型
if (vnode.type === Comment) {
current = null;
return vnode;
}
const comp = vnode.type; // TabA 组件定义
const name = getComponentName(
isAsyncWrapper(vnode)
? (vnode.type as ComponentOptions).__asyncResolved || {}
: comp
);
// 3. 检查是否需要缓存
const { include, exclude, max } = props;
if (
(include && !matches(include, name)) ||
(exclude && matches(exclude, name))
) {
vnode.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE;
current = vnode;
return rawVNode;
}
// 4. 生成缓存键并缓存组件
const key = vnode.key == null ? comp : vnode.key;
const cachedVNode = cache.get(key);
// 处理 vnode 复用的情况
if (vnode.el) {
vnode = cloneVNode(vnode);
if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
rawVNode.ssContent = vnode;
}
}
pendingCacheKey = key;
// 5. 处理缓存命中的情况
if (cachedVNode) {
// 复制已挂载的状态
vnode.el = cachedVNode.el;
vnode.component = cachedVNode.component;
// 更新过渡钩子
if (vnode.transition) {
setTransitionHooks(vnode, vnode.transition!);
}
// 标记为已缓存组件
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE;
// 更新 LRU 顺序
keys.delete(key);
keys.add(key);
} else {
// 新组件:添加到缓存
keys.add(key);
// 超出缓存上限时清理最旧的组件
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value!);
}
}
// 标记为 keep-alive 组件
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE;
// 更新当前组件引用
current = vnode;
return isSuspense(rawVNode.type) ? rawVNode : vnode;
};
首次渲染的主要流程:
-
渲染前准备:
- 重置缓存键
- 获取默认插槽内容
- 验证子组件数量和类型
-
组件处理:
- 获取实际渲染的组件 vnode
- 处理特殊类型(Comment、Suspense)
- 获取组件名称用于匹配
-
缓存判断:
- 检查 include/exclude 配置
- 决定是否需要缓存该组件
- 生成缓存键
-
状态处理:
- 处理组件复用情况
- 克隆 vnode 避免状态污染
- 设置缓存标记
-
缓存更新:
- 处理缓存命中的情况
- 维护 LRU 缓存顺序
- 控制缓存数量上限
3.3 组件切换过程
当切换到 TabB 时:
// 1. TabA 停用
sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!;
// 移动到隐藏容器
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense);
// 触发停用钩子
queuePostRenderEffect(() => {
instance.isDeactivated = true;
if (instance.da) {
invokeArrayFns(instance.da);
}
}, parentSuspense);
};
// 2. TabB 激活
sharedContext.activate = (vnode, container, anchor) => {
const instance = vnode.component!;
// 移动到目标容器
move(vnode, container, anchor, MoveType.ENTER, parentSuspense);
// 更新属性
patch(instance.vnode, vnode, container, anchor, instance, parentSuspense);
// 触发激活钩子
queuePostRenderEffect(() => {
instance.isDeactivated = false;
if (instance.a) {
invokeArrayFns(instance.a);
}
}, parentSuspense);
};
组件切换的处理流程:
-
停用当前组件:
- 将组件移动到隐藏容器
- 标记组件为停用状态
- 触发 deactivated 钩子
- 处理相关的 vnode 钩子
-
激活新组件:
- 移动到目标容器位置
- 更新组件属性和状态
- 触发 activated 钩子
- 更新组件树状态
-
优化处理:
- 使用移动而不是销毁重建
- 保持 DOM 结构完整性
- 维护组件实例状态
3.4 缓存更新和清理
// 1. 缓存组件
function cacheSubtree() {
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree));
}
}
// 在组件挂载和更新时缓存
onMounted(cacheSubtree);
onUpdated(cacheSubtree);
// 2. 清理缓存
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key) as VNode;
if (!current || !isSameVNodeType(cached, current)) {
unmount(cached);
}
cache.delete(key);
keys.delete(key);
}
// 3. 监听 include/exclude 变化
watch(
() => [props.include, props.exclude],
([include, exclude]) => {
include && pruneCache((name) => matches(include, name));
exclude && pruneCache((name) => !matches(exclude, name));
},
{ flush: "post", deep: true }
);
缓存管理的关键流程:
-
缓存更新:
- 在组件挂载时缓存
- 在组件更新时更新缓存
- 使用 pendingCacheKey 追踪待缓存组件
-
缓存清理:
- 根据 include/exclude 变化清理
- 超出 max 限制时清理最旧组件
- 卸载被清理的组件实例
-
缓存优化:
- 使用 Set 维护 LRU 顺序
- 动态响应配置变化
- 智能处理缓存更新时机
3.5 组件卸载
onBeforeUnmount(() => {
cache.forEach((cached) => {
unmount(cached);
});
});
卸载阶段的处理流程:
-
清理准备:
- 在组件卸载前触发
- 遍历所有缓存的组件
-
资源释放:
- 重置组件的 keepAlive 标记
- 执行组件的完整卸载流程
- 清理相关的 DOM 节点
-
收尾工作:
- 触发必要的生命周期钩子
- 确保所有缓存组件被正确清理
- 防止内存泄漏
4. 总结
KeepAlive 组件通过以下机制实现组件状态缓存:
-
缓存机制:
- 使用 Map 存储组件 vnode
- 使用 Set 维护 LRU 缓存顺序
- 支持最大缓存数限制
-
DOM 处理:
- 使用隐藏容器存储非活动组件
- 通过 move 操作切换组件
- 保持 DOM 结构完整
-
生命周期:
- 提供 activated/deactivated 钩子
- 在合适的时机触发钩子函数
- 支持嵌套组件的生命周期管理
-
性能优化:
- 避免重复创建组件实例
- 减少不必要的 DOM 操作
- 智能的缓存清理策略