- Contents
KeepAlive 的本质是缓存管理,再加上特殊的挂载/卸载逻辑。
<KeepAlive>
<CompA v-if="flag"></CompA>
<CompB v-else></CompB>
<button @click="flag=!flag">toggle</button>
</KeepAlive>
模版编译之后的结果:Teleport Explorer
import { resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock, createCommentVNode as _createCommentVNode, createElementVNode as _createElementVNode, KeepAlive as _KeepAlive } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_CompA = _resolveComponent("CompA")
const _component_CompB = _resolveComponent("CompB")
return (_openBlock(), _createBlock(_KeepAlive, null, [
(_ctx.flag)
// KeepAlive 的子节点创建的时候都添加了一个 key 的 prop
// 专门为 KeepAlive 的缓存设计的,这样每一个子节点都能有一个唯一的 key
? (_openBlock(), _createBlock(_component_CompA, { key: 0 }))
: (_openBlock(), _createBlock(_component_CompB, { key: 1 })),
_createElementVNode("button", {
onClick: $event => (_ctx.flag=!_ctx.flag)
}, "toggle", 8 /* PROPS */, ["onClick"])
], 1024 /* DYNAMIC_SLOTS */))
}
KeepAlive 组件对这两个组件做了一层封装,KeepAlive 是一个抽象组件,它并不会渲染成一个真实的 DOM,只会渲染内部包裹的子节点,并且让内部的子组件在切换的时候,不会走一整套递归卸载和挂载 DOM 的流程(而是走激活和失活流程)。
- deactivate 失活:将 KeepAlive 组件从原容器中移动到另一个隐藏容器中,实现「假卸载」
- activate 激活:将该组件从隐藏容器中再搬运到原容器中
缓存管理使用 LRU Cache
组件渲染
函数先从 slots.default()
拿到子节点 children
,它就是 KeepAlive 组件包裹的子组件,由于 KeepAlive 只能渲染单个子节点,所以当 children
长度大于 1
的时候会报警告。
先不考虑缓存情况下,KeepAlive 渲染的 vnode 就是子节点 children
的第一个元素,它是函数的返回值。
KeepAlive 是抽象组件,它本身不渲染成实体节点,而是渲染它的第一个子节点。
缓存的设计
KeepAlive 内注入了两个钩子函数,onMounted
和 onUpdated
,在这两个钩子函数内部都执行了 cacheSubtree
函数来做缓存。
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
由于 pendingCacheKey
是在 KeepAlive 的 render 函数中才会被赋值,所以 KeepAlive 首次进入 onMounted
钩子函数的时候是不会缓存的。
然后 KeepAlive 执行 render
的时候,pendingCacheKey
会被赋值为 vnode.key
,渲染后的模板。(KeepAlive 的子节点创建的时候都添加了一个 key
的 prop
,它就是专门为 KeepAlive 的缓存设计的,这样每一个子节点都能有一个唯一的 key
。请见:上面示例渲染后的模板)
有了缓存的渲染子树后,我们就可以直接拿到它对应的 DOM 以及组件实例 component
,赋值给 KeepAlive 的 vnode
,并更新 vnode.shapeFlag
,以便后续 patch
阶段使用。
由于 KeepAlive 缓存的都是有状态的组件 vnode
,有缓存和没缓存在 patch
和 unmount
阶段有何区别?
patch
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
n2.slotScopeIds = slotScopeIds
if (n1 == null) {
// 处理 KeepAlive 组件
if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
parentComponent.ctx.activate(n2, container, anchor, isSVG, optimized)
}
else {
// 挂载组件
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
}
else {
// 更新组件
}
}
KeepAlive 首次渲染某一个子节点时,和正常的组件节点渲染没有区别。
但是有缓存后,由于标记了 shapeFlag
,所以在执行 processComponent
函数时会走到处理 KeepAlive 组件的逻辑中,执行 KeepAlive 组件实例上下文中的 activate
激活函数(sharedContext.activate
)。
sharedContext.activate
中可以看出由于此时已经能从 vnode.el
中拿到缓存的 DOM 了,所以可以直接调用 move
方法挂载节点(而不是重新挂载),然后执行 patch
方法更新组件,以防止 props
发生变化的情况。接下来,就是通过 queuePostRenderEffect
的方式,在组件渲染完毕后,执行子节点组件定义的 activated
钩子函数。
unmount
类似的还有 unmount 阶段
const unmount = (
vnode,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false
) => {
const {
type,
props,
ref,
children,
dynamicChildren,
shapeFlag,
patchFlag,
dirs
} = vnode
// unset ref
if (ref != null) {
setRef(ref, null, parentSuspense, vnode, true)
}
// 处理 KeepAlive 组件
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
return
}
// 省略代码
}
有缓存标记 shapeFlag
之后在执行 unmount
函数时会走到处理 KeepAlive 组件的逻辑中,执行 KeepAlive 组件实例上下文中的 deactivate
失活函数(sharedContext.deactivate
)。
组件并不会被卸载,而是调用 move
方法挂载到 storageContainer
隐藏容器内,然后执行 queuePostRenderEffect
的方式
缓存管理 - LRU Cache
Vue.js 对于缓存所采用的修剪策略是:LRU Cache,可以通过 max
prop 来设置。
<KeepAlive :max="2">
<Comp :is="dynamicComp" />
</KeepAlive>
源码实现:
const keys: Keys = new Set()
if (cachedVNode) {
// 有缓存
// 省略代码
// make this key the freshest
// 先删除已有 key,再重新增加,保证最新
keys.delete(key)
keys.add(key)
} else {
// 无缓存
// 直接新增
keys.add(key)
// prune oldest entry
// 超过 max 限制,则清缓存
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key) as VNode
if (!current || cached.type !== current.type) {
// 真正的卸载组件
unmount(cached)
} else if (current) {
// current active instance should no longer be kept-alive.
// we can't unmount it now but it might be later, so reset its flag now.
// 修改 vnode 的 shapeFlag
// 不让它再被当作一个 KeepAlive 的 vnode
// 这样就可以走正常的卸载逻辑
resetShapeFlag(current)
}
cache.delete(key)
keys.delete(key)
}
卸载
除 KeepAlive 组件本身的 unmount 之外,当 KeepAlive 所在的组件卸载时,由于卸载的递归特性,也会触发 KeepAlive 组件的卸载,在卸载的过程中会执行 onBeforeUnmount
钩子函数
onBeforeUnmount(() => {
cache.forEach((cached) => {
const { subTree, suspense } = instance
const vnode = getInnerChild(subTree)
if (cached.type === vnode.type) {
// current instance will be unmounted as part of keep-alive's unmount
resetShapeFlag(vnode)
// but invoke its deactivated hook here
const da = vnode.component!.da
da && queuePostRenderEffect(da, suspense)
return
}
unmount(cached)
})
})
遍历所有缓存的 vnode,并且比对缓存的 vnode
是不是当前 KeepAlive 组件渲染的 vnode
- 是,则执行
resetShapeFlag
方法(修改vnode
的shapeFlag
,不让它再被当作一个 KeepAlive 的 vnode,这样就可以走正常的卸载逻辑)。接着通过queuePostRenderEffect
的方式执行子组件的deactivated
钩子函数。 - 不是,则执行
unmount
方法重置shapeFlag
以及执行缓存vnode
的整套卸载流程
const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,
// Marker for special handling inside the renderer. We are not using a ===
// check directly on KeepAlive in the renderer, because importing it directly
// would prevent it from being tree-shaken.
__isKeepAlive: true,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number],
},
setup(props: KeepAliveProps, { slots }: SetupContext) {
// 当前 KeepAlive 组件的实例
const instance = getCurrentInstance()!
// KeepAlive communicates with the instantiated renderer via the
// ctx where the renderer passes in its internals,
// and the KeepAlive instance exposes activate/deactivate implementations.
// The whole point of this is to avoid importing KeepAlive directly in the
// renderer to facilitate tree-shaking.
// 对于 KeepAlive 组件来说,它的实例上存在特殊的 keepAliveCtx 对象,该对象由渲染器注入
// 该对象会暴露渲染器的一些内部方法,例如其中 move 函数用来将一段 DOM 移动到另一个容器中
const sharedContext = instance.ctx as KeepAliveContext
// if the internal renderer is not registered, it indicates that this is server-side rendering,
// for KeepAlive, we just need to render its children
if (__SSR__ && !sharedContext.renderer) {
return () => {
const children = slots.default && slots.default()
return children && children.length === 1 ? children[0] : children
}
}
// 创建一个缓存对象
// key: vnode.type
// value: vnode
const cache: Cache = new Map()
const keys: Keys = new Set()
let current: VNode | null = null
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
;(instance as any).__v_cache = cache
}
const parentSuspense = instance.suspense
const {
renderer: {
p: patch,
m: move,
um: _unmount,
o: { createElement },
},
} = sharedContext
// 创建隐藏容器
const storageContainer = createElement('div')
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
const instance = vnode.component!
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// in case props have changed
patch(
instance.vnode,
vnode,
container,
anchor,
instance,
parentSuspense,
isSVG,
vnode.slotScopeIds,
optimized
)
queuePostRenderEffect(() => {
instance.isDeactivated = false
if (instance.a) {
invokeArrayFns(instance.a)
}
const vnodeHook = vnode.props && vnode.props.onVnodeMounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
}, parentSuspense)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
// Update components tree
devtoolsComponentAdded(instance)
}
}
sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
queuePostRenderEffect(() => {
if (instance.da) {
invokeArrayFns(instance.da)
}
const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
instance.isDeactivated = true
}, parentSuspense)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
// Update components tree
devtoolsComponentAdded(instance)
}
}
function unmount(vnode: VNode) {
// reset the shapeFlag so it can be properly unmounted
resetShapeFlag(vnode)
_unmount(vnode, instance, parentSuspense, true)
}
function pruneCache(filter?: (name: string) => boolean) {
cache.forEach((vnode, key) => {
const name = getComponentName(vnode.type as ConcreteComponent)
if (name && (!filter || !filter(name))) {
pruneCacheEntry(key)
}
})
}
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key) as VNode
if (!current || cached.type !== current.type) {
unmount(cached)
} else if (current) {
// current active instance should no longer be kept-alive.
// we can't unmount it now but it might be later, so reset its flag now.
resetShapeFlag(current)
}
cache.delete(key)
keys.delete(key)
}
// prune cache on include/exclude prop change
watch(
() => [props.include, props.exclude],
([include, exclude]) => {
include && pruneCache((name) => matches(include, name))
exclude && pruneCache((name) => !matches(exclude, name))
},
// prune post-render after `current` has been updated
{ flush: 'post', deep: true }
)
// cache sub tree after render
let pendingCacheKey: CacheKey | null = null
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
onBeforeUnmount(() => {
cache.forEach((cached) => {
const { subTree, suspense } = instance
const vnode = getInnerChild(subTree)
if (cached.type === vnode.type) {
// current instance will be unmounted as part of keep-alive's unmount
resetShapeFlag(vnode)
// but invoke its deactivated hook here
const da = vnode.component!.da
da && queuePostRenderEffect(da, suspense)
return
}
unmount(cached)
})
})
return () => {
pendingCacheKey = null
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
} else if (
!isVNode(rawVNode) ||
(!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
!(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
) {
current = null
return rawVNode
}
let vnode = getInnerChild(rawVNode)
const comp = vnode.type as ConcreteComponent
// for async components, name check should be based in its loaded
// inner component if available
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))
) {
current = vnode
return rawVNode
}
const key = vnode.key == null ? comp : vnode.key
// 挂载时先获取缓存的组件 vnode
const cachedVNode = cache.get(key)
// clone vnode if it's reused because we are going to mutate it
if (vnode.el) {
vnode = cloneVNode(vnode)
if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
rawVNode.ssContent = vnode
}
}
// #1513 it's possible for the returned vnode to be cloned due to attr
// fallthrough or scopeId, so the vnode here may not be the final vnode
// that is mounted. Instead of caching it directly, we store the pending
// key and cache `instance.subTree` (the normalized vnode) in
// beforeMount/beforeUpdate hooks.
pendingCacheKey = key
if (cachedVNode) {
// 有缓存,则不挂载,而走激活
// copy over mounted state
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
if (vnode.transition) {
// recursively update transition hooks on subTree
setTransitionHooks(vnode, vnode.transition!)
}
// 避免 vnode 节点作为新节点被挂载
// avoid vnode being mounted as fresh
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// make this key the freshest
keys.delete(key)
keys.add(key)
} else {
keys.add(key)
// prune oldest entry
// 删除最久不用的 key
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
// avoid vnode being unmounted
// 避免 vnode 被卸载
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode
return isSuspense(rawVNode.type) ? rawVNode : vnode
}
},
}