前言
我们知道Vue内置了KeepAlive组件,帮助我们在卸载组件的时候对组件进行缓存,让我们在多个组件进行切换的场景下避免了组件销毁,再重新创建带来的性能损耗。示例代码如下:
<template>
<KeepAlive>
<component :is="activeComponent" />
</KeepAlive>
</template>
当动态组件在随着 activeComponent 变化时,如果没有 KeepAlive 做缓存,那么组件在来回切换时就会进行重复的实例化,这里就是通过 KeepAlive 实现了对不活跃组件的缓存。
源码分析
接下来我们来看一下keepAlive的源码
const KeepAliveImpl = {
// 组件名称
name: `KeepAlive`,
// 区别于其他组件的标记
__isKeepAlive: true,
// 组件的 props 定义
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
setup(props, {slots}) {
// ...
// setup 返回一个函数
return () => {
// ...
}
}
const isKeepAlive = vnode => vnode.type.__isKeepAlive
可以看到,KeepAlive 组件中,通过 __isKeepAlive 属性来完成对这个内置组件的特殊标记,这样外部可以通过 isKeepAlive 函数来做区分。
然后定义了keepAlive中的几个props
include表示包含哪些组件可被缓存exclude表示排除那些组件max表示最大的缓存组件数
KeepAlive 的 render 函数
先来看看 render 函数的源码实现:
setup(props: KeepAliveProps, { slots }: SetupContext) {
// 获取组件实例
const instance = getCurrentInstance()!
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 (!sharedContext.renderer) {
return slots.default
}
const cache: Cache = new Map()
const keys: Keys = new Set()
let current: VNode | null = null
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!
// 将缓存的组件挂载到容器中
// activate 激活函数,核心就是通过 move 方法,将缓存中的 vnode 节点直接挂载到容器中
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// in case props have changed
// 如果 props 有变动,还是需要对 props 进行 patch
patch(
instance.vnode,
vnode,
container,
anchor,
instance,
parentSuspense,
isSVG,
vnode.slotScopeIds,
optimized
)
// 执行组件的钩子函数
queuePostRenderEffect(() => {
instance.isDeactivated = false
if (instance.a) {
// 执行 onActivated 钩子
invokeArrayFns(instance.a)
}
// 执行 onVnodeMounted 钩子
const vnodeHook = vnode.props && vnode.props.onVnodeMounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
}, parentSuspense)
}
// keepAlive组件的deactive方法,卸载态函数 deactivate
//核心工作就是将页面中的 DOM 移动到一个隐藏不可见的容器 storageContainer 当中,
// 这样页面中的元素就被移除了
sharedContext.deactivate = (vnode: VNode) => {
// 获取组件实例
const instance = vnode.component!
// 将组件移动到隐藏容器中
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
// 执行组件的钩子函数,将用户定义的 onDeactivated 钩子放到状态更新流程后执行
queuePostRenderEffect(() => {
// 执行组件的 onDeactivated 钩子
if (instance.da) {
invokeArrayFns(instance.da)
}
// 执行 onVnodeUnmounted
const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
// 执行完设置组件状态为isDeactivated
instance.isDeactivated = true
}, parentSuspense)
}
function unmount(vnode: VNode) {
// reset the shapeFlag so it can be properly unmounted
resetShapeFlag(vnode)
_unmount(vnode, instance, parentSuspense)
}
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
//定义了一个 pendingCacheKey 变量,用来作为 cache 的缓存 key
let pendingCacheKey: CacheKey | null = null
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
// 以 pendingCacheKey 作为key 进行缓存收集
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)
})
})
// setup 返回一个函数
return () => {
// 记录需要被缓存的 key
pendingCacheKey = null
if (!slots.default) {
return null
}
// 获取子节点
const children = slots.default()
// rawVNode是keepAlive包裹的第一个子节点
const rawVNode = children[0]
if (children.length > 1) {
if (__DEV__) {
warn(`KeepAlive should contain exactly one component child.`)
}
// 子节点数量大于 1 个,不会进行缓存,直接返回
current = null
return children
} else if (
!isVNode(rawVNode) ||
(!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
!(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
) {
current = null
return rawVNode
}
// suspense 特殊处理,正常节点就是返回节点 vnode
let vnode = getInnerChild(rawVNode)
const comp = vnode.type as ConcreteComponent
// 获取 Component.name 值
const name = getComponentName(comp)
// 获取 props 中的属性
const { include, exclude, max } = props
// 如果组件 name 不在 include 中或者存在于 exclude 中,则直接返回
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
current = vnode
return rawVNode
}
// 缓存相关,定义缓存 key
const key = vnode.key == null ? comp : vnode.key
// 从缓存中取值
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.
// 给 pendingCacheKey 赋值,将在 beforeMount/beforeUpdate 中被使用
pendingCacheKey = key
// 如果存在缓存的 vnode 元素
if (cachedVNode) {
// 复制挂载状态
// 复制 DOM
vnode.el = cachedVNode.el
// 复制 component
vnode.component = cachedVNode.component
if (vnode.transition) {
// recursively update transition hooks on subTree
setTransitionHooks(vnode, vnode.transition!)
}
// avoid vnode being mounted as fresh
// 增加 shapeFlag 类型 COMPONENT_KEPT_ALIVE
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// make this key the freshest
// 把缓存的 key 移动到到队首
keys.delete(key)
keys.add(key)
} else {
// 如果缓存不存在,则添加缓存
keys.add(key)
// prune oldest entry
// 如果超出了最大的限制,则移除最早被缓存的值
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
// avoid vnode being unmounted
// 增加 shapeFlag 类型 COMPONENT_SHOULD_KEEP_ALIVE,避免被卸载
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode
// 返回 vnode 节点
return rawVNode
}
}
可以看到返回的这个 render 函数执行的结果就是返回被 KeepAlive 包裹的子节点的 vnode 只不过在返回子节点的过程中做了很多处理而已,如果子节点数量大于一个,那么将不会被 keepAlive,直接返回子节点的 vnode,如果组件 name 不在用户定义的 include 中或者存在于 exclude 中,也会直接返回子节点的 vnode。
缓存设计
接着来看后续的缓存步骤
// cache sub tree after render
//定义了一个 pendingCacheKey 变量,用来作为 cache 的缓存 key
let pendingCacheKey: CacheKey | null = null
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
// 以pendingCacheKey作为key 进行缓存收集
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
首先定义了一个 pendingCacheKey 变量,用来作为 cache 的缓存 key。在初始化 KeepAlive 组件的时候,此时还没有缓存,那么只会将 key 添加到 keys 这样一个 Set 中,在组件的 onMounted 和 onUpdated 钩子中进行缓存组件的 vnode 收集,因为这个时候收集到的 vnode 节点是稳定不会变的。
另外,注意到 props 中还有一个 max 变量用来标记最大的缓存数量,这个缓存策略就是类似于LRU的方式实现的。在缓存重新被激活时,之前缓存的 key 会被重新添加到Set的前面,标记为最近的一次缓存,如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。
最后,当缓存的节点被重新激活时,则会将缓存中的节点的 el 属性赋值给新的 vnode 节点,从而减少了再进行 patch 生成 DOM 的过程,这里也说明了 KeepAlive 核心目的就是缓存 DOM 元素。
激活态设计
上述源码中,当组件被添加到 KeepAlive 缓存池中时,也会为 vnode 节点的 shapeFlag 添加两额外的两个属性,分别是 COMPONENT_KEPT_ALIVE 和 COMPONENT_SHOULD_KEEP_ALIVE。我们先说 COMPONENT_KEPT_ALIVE 这个属性,当一个节点被标记为 COMPONENT_KEPT_ALIVE 时,会在 processComponent 时进行特殊处理:
const processComponent = (...) => {
if (n1 == null) {
// 处理 KeepAlive 组件
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
// 执行 activate 钩子
;(parentComponent!.ctx as KeepAliveContext).activate(
n2, container, anchor, isSVG, optimized )
} else {
mountComponent( n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
}
else { // 更新组件 }
}
可以看到,在 processComponent 阶段如果是 keepAlive 的组件,在挂载过程中,不会执行执行 mountComponent 的逻辑,因为已经缓存好了,所以只需要再次调用 activate 激活就好了。接下来看看这个激活函数做了哪些事
setup(props: KeepAliveProps, { slots }: SetupContext) {
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
// 获取组件实例
const instance = vnode.component!
// 将缓存的组件挂载到容器中
// activate 激活函数,核心就是通过 move 方法,将缓存中的 vnode 节点直接挂载到容器中
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// in case props have changed
// 如果 props 有变动,还是需要对 props 进行 patch
patch(
instance.vnode,
vnode,
container,
anchor,
instance,
parentSuspense,
isSVG,
vnode.slotScopeIds,
optimized
)
// 执行组件的钩子函数
queuePostRenderEffect(() => {
instance.isDeactivated = false
if (instance.a) {
// 执行 onActivated 钩子
invokeArrayFns(instance.a)
}
// 执行 onVnodeMounted 钩子
const vnodeHook = vnode.props && vnode.props.onVnodeMounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
}, parentSuspense)
}
}
可以直观的看到 activate 激活函数,核心就是通过 move 方法,将缓存中的 vnode 节点直接挂载到容器中,同时为了防止 props 变化导致组件变化,也会执行 patch 方法来更新组件。当这一切都执行完成后,最后再通过 queuePostRenderEffect 函数,将用户定义的 onActivated 钩子放到状态更新流程后执行。
卸载态设计
接下来我们再看另一个标记态:COMPONENT_SHOULD_KEEP_ALIVE,我们看一下组件的卸载函数 unmount 的设计:
const unmount = (vnode, parentComponent, parentSuspense, doRemove = false) => {
// ...
const { shapeFlag } = vnode
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
return
}
// ...
}
可以看到,如果 shapeFlag 上存在 COMPONENT_SHOULD_KEEP_ALIVE 属性的话,那么将会执行 ctx.deactivate 方法,我们再来看一下 deactivate 函数的定义:
setup(props: KeepAliveProps, { slots }: SetupContext) {
// 创建一个隐藏容器
const storageContainer = createElement('div')
// keepAlive组件的deactive方法,卸载态函数 deactivate 核心工作就是将页面中的 DOM 移动到一个隐藏不可见的容器 storageContainer 当中,这样页面中的元素就被移除了
sharedContext.deactivate = (vnode: VNode) => {
// 获取组件实例
const instance = vnode.component!
// 将组件移动到隐藏容器中
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
// 执行组件的钩子函数,将用户定义的 onDeactivated 钩子放到状态更新流程后执行
queuePostRenderEffect(() => {
// 执行组件的 onDeactivated 钩子
if (instance.da) {
invokeArrayFns(instance.da)
}
// 执行 onVnodeUnmounted
const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
// 执行完设置组件状态为isDeactivated
instance.isDeactivated = true
}, parentSuspense)
}
}
卸载态函数 deactivate 核心工作就是将页面中的 DOM 移动到一个隐藏不可见的容器 storageContainer 当中,这样页面中的元素就被移除了。当这一切都执行完成后,最后再通过 queuePostRenderEffect 函数,将用户定义的 onDeactivated 钩子放到状态更新流程后执行。
总结
- 组件是通过类似于
LRU的缓存机制来缓存的,并为缓存的组件vnode的shapeFlag属性打上COMPONENT_KEPT_ALIVE属性,当组件在processComponent挂载时,如果存在COMPONENT_KEPT_ALIVE属性,则会执行激活函数,激活函数内执行具体的缓存节点挂载逻辑。 - 缓存不是越多越好,因为所有的缓存节点都会被存在
cache中,如果过多,则会增加内存负担。 - 处理缓存的方式就是在缓存重新被激活时,之前缓存的
key会被重新添加到Set集合的头部,标记为最近的一次缓存,如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被丢弃。