keepalive帮助我们保存组件的状态,它的原理并不复杂,就是保存默认插槽传入的组件在卸载时的状态,包括保存子组件的组件实例和el,我们直接看源码:
packages/runtime-core/src/components/KeepAlive.ts
// KeepAlive组件
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) {
// 获取当前实例
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.
// instance.ctx赋值给sharedContext
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
}
}
// 声明一个用于缓存的map key是子组件对象 值是subTree(子组件render执行后的vnode)
const cache: Cache = new Map()
// keys用来存储cache的key
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
// 创建一个div容器 用于存放子组件的el
const storageContainer = createElement('div')
// 挂载activate方法 在patch子组件的过程中判断shapeFlag为COMPONENT_KEPT_ALIVE时会执行该方法
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
// 组件实例
const instance = vnode.component!
// move的作用是将vnode.el移动到container中 以anchor为锚点执行insertBefore
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// in case props have changed
// patch防止props发生改变
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)
}
}
// 挂载deactivate方法 组件卸载时判断shapeFlag为COMPONENT_SHOULD_KEEP_ALIVE时调用
sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
// 将vnode.el挂载到storageContainer(起到缓存el的作用)
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)
}
})
}
// 根据key移除对应的缓存
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key) as VNode
if (!current || !isSameVNodeType(cached, current)) {
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
// 深度监听include和exclude 当include或exclude发生改变及时调整缓存
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
// 缓存subTree
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
// 生命周期钩子 执行cacheSubtree
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
// keepalive 组件卸载前清除所有缓存
onBeforeUnmount(() => {
cache.forEach(cached => {
const { subTree, suspense } = instance
const vnode = getInnerChild(subTree)
if (cached.type === vnode.type && cached.key === vnode.key) {
// 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)
})
})
// keepalive render函数
return () => {
pendingCacheKey = null
if (!slots.default) {
return null
}
// 执行默认插槽函数 获取插槽内容vnode数组
const children = slots.default()
// 子组件vnode
const rawVNode = children[0]
// keep-alive只能有一个子节点
// 如果有多个子节点或者子节点不是有状态组件且不是suspense组件 则直接返回子节点
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
}
// 如果是suspense组件 返回ssContent
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
// 获取组件name
const name = getComponentName(
isAsyncWrapper(vnode)
? (vnode.type as ComponentOptions).__asyncResolved || {}
: comp
)
const { include, exclude, max } = props
// 如果存在include且include中没有匹配到name 或者
// 存在exclude且exclude中匹配到了name 直接返回子节点 不做缓存
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
current = vnode
return rawVNode
}
// 组件使用组件对象作为key
const key = vnode.key == null ? comp : vnode.key
// 从cache中获取缓存的vnode
const cachedVNode = cache.get(key)
// clone vnode if it's reused because we are going to mutate it
// clone vnode 如果它被重用,因为我们要改变它
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.
// 等待被缓存的key
pendingCacheKey = key
if (cachedVNode) {
// 如果已经被缓存 将缓存vnode的el和组件实例赋值给当前vnode
// 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!)
}
// avoid vnode being mounted as fresh
// 将vnode标记为COMPONENT_KEPT_ALIVE
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// make this key the freshest
// 这里删除再添加是因为set数据类型会记录key的插入顺序
// 这里将key的插入顺序更新为最新 以便后续删除最久没有被访问的组件
keys.delete(key)
keys.add(key)
} else {
// 没有被缓存
// 添加到keys
keys.add(key)
// prune oldest entry
// 如果keys的长度大于max 则删除最久没有被访问的组件
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
// avoid vnode being unmounted
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode
return isSuspense(rawVNode.type) ? rawVNode : vnode
}
}
}
逻辑梳理:首先是keepalive组件挂载,首先执行其setup函数,里面声明了用于存储子组件vnode的cache和用于存储子组件对象的keys(这个keys是实现LUR缓存的关键)还有用于存储子组件el的storageContainer,挂载了activate和deactivate方法,在子组件的挂载和卸载时调用,主要用来移动el进行缓存或者复用。由于在keepalive的生命周期中setup只会调用一次,所以不用担心这些数据会被清除覆盖。后续就是setup的返回值处理,因为返回值是一个函数,所以这个函数会被作为keepalive的render函数调用。
keepalive渲染函数做的事情大致是获取子组件vnode,判断vnode是否已经被缓存,如果没有则添加到缓存(这个逻辑是在keepalive mounted或updated生命周期执行的),且vnode的shapFlag会被标记为ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE,组件卸载时会调用keepalive ctx上挂载的deactivate方法,以保存子组件的el;如果已经被缓存,则将缓存子组件vnode的el和component(被缓存的组件实例)赋值给新的子组件vnode且将子组件shapeFlag标记为ShapeFlags.COMPONENT_KEPT_ALIVE,这样在子组件的processComponent中就会判断这个标记然后调用keepalive ctx上挂载的activate方法,将el再移动回现在的父容器,这样就可以避免子组件的重新执行mountComponent重新挂载组件。这样就实现了子组件的缓存。
还有就是关于指定max的LRU缓存的实现,其利用了Set数据结构的插入会记录插入顺序的特性。所以在组件实例缓存被访问时会进行delete再add的操作以更新该组件被记录的顺序,当缓存超出max时keys.values().next().value就是最久没有被激活的缓存,所以删除这个key的缓存即可。