14.1.1 组件的激活与失活
KeepAlive 让被切走的组件不被真正销毁:把组件渲染的内容从原容器「搬进」一个隐藏容器,需要时再「搬回来」,对应生命周期钩子 activated / deactivated。
一、KeepAlive 是什么
- 名字借鉴 HTTP 协议的 KeepAlive(HTTP 持久连接):允许多个请求/响应共用一个 TCP 连接,避免频繁销毁、重建连接带来的额外开销。
- 组件层面的 KeepAlive 要解决的是同一类问题:组件反复「销毁 + 重新创建」的开销。
- 实现本质:缓存管理 + 特殊的挂载 / 卸载逻辑。
二、核心思路:假卸载与假挂载
| 时机 | 普通组件 | 被 KeepAlive 的组件 |
|---|---|---|
| 切走 | 真卸载:移除 DOM、销毁组件实例 | 假卸载:把内容移动到隐藏容器,实例与状态保留 |
| 切回 | 真挂载:新建实例、重新创建 DOM | 假挂载:把内容从隐藏容器移回原容器,复用原实例 |
| 生命周期 | unmounted / mounted | deactivated / activated |
为此渲染器要提供两个能力:
- move:把一段内容(组件渲染出的 DOM)移动到另一个容器;
- createElement:创建一个不插入文档的隐藏容器。
关键:KeepAlive 不直接操作
document,而是借用渲染器注入的move,所以自定义渲染器同样能生效(见第五节)。
三、渲染器侧的改造
1. 给 KeepAlive 组件实例注入 keepAliveCtx
挂载组件时,如果发现 vnode.type.__isKeepAlive 为真,就往组件实例上挂一个 keepAliveCtx:
// 组件实例
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
slots,
mounted: [],
unmounted: [],
keepAliveCtx: null // 新增:只有 KeepAlive 组件的实例才会有该属性
}
// 检查当前要挂载的组件是否是 KeepAlive 组件
const isKeepAlive = vnode.type.__isKeepAlive
if (isKeepAlive) {
// 在 KeepAlive 组件实例上添加 keepAliveCtx 对象,由渲染器注入
instance.keepAliveCtx = {
// move:把 vnode 所描述的内容移动到指定容器
move: (vnode, container, anchor) => {
// 本质上是把组件渲染的内容(子树的真实 DOM)移动过去
insert(vnode.component.subTree.el, container, anchor)
},
createElement
}
}
2. patch:切回时改走「激活」而不是挂载
// patch 中处理组件类型 vnode 的分支
if (!n1) {
// 该组件已经被 KeepAlive 过:不重新挂载,而是激活
if (n2.keptAlive) {
n2.keepAliveInstance._activate(n2, container, anchor)
} else {
mountComponent(n2, container, anchor)
}
} else {
patchComponent(n1, n2, container)
}
3. unmount:该缓存的组件不真卸载
function unmount(vnode) {
if (vnode.type === Fragment) {
vnode.children.forEach(c => unmount(c))
return
}
if (typeof vnode.type === 'object') {
// shouldKeepAlive 标识该组件是否应该被 KeepAlive
if (vnode.shouldKeepAlive) {
// 不真卸载,交给它的父组件(KeepAlive)使其失活
vnode.keepAliveInstance._deActivate(vnode)
} else {
vnode.component.unmounted.forEach(hook => hook())
unmount(vnode.component.subTree)
}
return
}
const parent = vnode.el.parentNode
if (parent) parent.removeChild(vnode.el)
}
四、KeepAlive 组件实现
const KeepAlive = {
// KeepAlive 组件独有的属性,用作标识
__isKeepAlive: true,
setup(props, { slots }) {
// 缓存对象:key 为 vnode.type,value 为 vnode
const cache = new Map()
// 当前 KeepAlive 组件的实例
const instance = currentInstance
// 渲染器注入的 keepAliveCtx:move 用于把一段内容移动到另一个容器
const { move, createElement } = instance.keepAliveCtx
// 创建隐藏容器(不挂到文档上)
const storageContainer = createElement('div')
// 两个内部函数会被渲染器调用
instance._deActivate = (vnode) => {
// 失活:移动到隐藏容器
move(vnode, storageContainer)
}
instance._activate = (vnode, container, anchor) => {
// 激活:移动回原容器
move(vnode, container, anchor)
}
return () => {
// 默认插槽的内容就是要被 KeepAlive 的组件
const rawVNode = slots.default()
// 非组件的 vnode 无法被 KeepAlive,直接渲染
if (typeof rawVNode.type !== 'object') return rawVNode
// 挂载时先查缓存
const cachedVNode = cache.get(rawVNode.type)
if (cachedVNode) {
// 命中缓存 → 不挂载,而是激活
rawVNode.component = cachedVNode.component // 继承组件实例
rawVNode.keptAlive = true // 告诉渲染器不要重新挂载
} else {
// 未命中 → 写入缓存,下次切回就不用重新挂载了
cache.set(rawVNode.type, rawVNode)
}
// 告诉渲染器:这个组件不该被真卸载
rawVNode.shouldKeepAlive = true
// 把 KeepAlive 组件实例也挂到 vnode 上,方便渲染器里访问
rawVNode.keepAliveInstance = instance
return rawVNode
}
}
}
几个标记的作用:
| 标记 | 写在哪 | 谁读 | 作用 |
|---|---|---|---|
__isKeepAlive | KeepAlive 组件选项 | 渲染器 mountComponent | 识别 KeepAlive,注入 keepAliveCtx |
keptAlive | 内部组件 vnode | 渲染器 patch | 已缓存 → 调 _activate 而非 mountComponent |
shouldKeepAlive | 内部组件 vnode | 渲染器 unmount | 不真卸载 → 调 _deActivate |
keepAliveInstance | 内部组件 vnode | 渲染器 patch / unmount | 拿到 KeepAlive 实例,调用 _activate / _deActivate |
五、activated / deactivated 的调用时机
| 钩子 | 触发时机 | 说明 |
|---|---|---|
| activated | 组件被从隐藏容器移回页面时(即 _activate 中) | 首次挂载也会触发;组件已在页面上的更新不触发 |
| deactivated | 组件被移入隐藏容器时(即 _deActivate 中) | 此后 unmounted 不会触发,因为组件并未真正卸载 |
即:
instance._activate = (vnode, container, anchor) => {
move(vnode, container, anchor) // 先搬回来
vnode.component.activated.forEach(hook => hook()) // 再触发 activated
}
instance._deActivate = (vnode) => {
vnode.component.deactivated.forEach(hook => hook()) // 先触发 deactivated
move(vnode, storageContainer) // 再搬走
}
易错点:被 KeepAlive 的组件只会经历
activated/deactivated,不会再触发mounted/unmounted(除首次挂载那一次)。
补充:官方实现用 vnode 的
shapeFlag上的 COMPONENT_SHOULD_KEEP_ALIVE 位来做「不该真卸载」的判断,本书为教学简化成vnode.shouldKeepAlive布尔属性,语义一致。
六、为什么自定义渲染器也能生效
_activate/_deActivate里没有出现任何document/DOMAPI,移动能力来自渲染器注入的keepAliveCtx.move;move内部用的是渲染器选项里的insert,因此换成 canvas、小程序等自定义渲染器时,KeepAlive 无需改动;- 隐藏容器也是用渲染器的
createElement创建的,同样与平台无关。
这就是「内建组件 + 渲染器内部方法注入」的分工:内建组件只写通用逻辑,平台相关能力全部由渲染器提供。
相关
- 14.1.2 include 和 exclude:让用户用正则显式指定哪些组件该缓存、哪些不该缓存
- 14.1.3 缓存管理:缓存只增不减怎么办 ——
max阈值与淘汰策略