keep-alive
组件是Vue
提供的组件,它可以缓存组件实例,在某些情况下避免了组件的挂载和卸载,在某些场景下非常实用。
例如最近我们遇到了一种场景,某个组件上传较大的文件是个耗时的操作,如果上传的时候切换到其他页面内容,组件会被卸载,对应的下载也会被取消。此时可以用keep-alive
组件包裹这个组件,在切换到其他页面时该组件仍然可以继续上传文件,切换回来也可以看到上传进度。
keep-alive
渲染子节点
const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,
setup(props: KeepAliveProps, { slots }: SetupContext) {
// 需要渲染的子树VNode
let current: VNode | null = null
return () => {
// 获取子节点, 由于Keep-alive只能有一个子节点,直接取第一个子节点
const children = slots.default()
const rawVNode = children[0]
// 标记 | ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE,这个组件是`keep-alive`组件, 这个标记 不走 unmount逻辑,因为要被缓存的
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
// 记录当前子节点
current = vnode
// 返回子节点,代表渲染这个子节点
return rawVNode
}
}
}
组件的
setup
返回函数,这个函数就是组件的渲染函数;
keep-alive
是一个虚拟节点不需要渲染,只需要渲染子节点,所以函数只需要返回子节点VNode就行了。
缓存功能
- 定义存储缓存数据的
Map
, 所有的缓存键值数组Keys
,代表当前子组件的缓存键值pendingCacheKey
;
const cache = new Map()
const keys: Keys = new Set()
let pendingCacheKey: CacheKey | null = null
- 渲染函数中获取子树节点
VNode
的key
, 缓存cache
中查看是否有key
对应的缓存节点
const key = vnode.key
const cachedVNode = cache.get(key)
key
是生成子节点的渲染函数时添加的,一般情况下就是0,1,2,…这些数字。
- 记录下点前的
key
pendingCacheKey = key
- 如果有找到缓存的
cachedVNode
节点,将缓存的cachedVNode
节点的组件实例和节点元素 复制给新的VNode
节点。没有找到就先将当前子树节点VNode
的pendingCacheKey
加入到Keys
中。
if (cachedVNode) {
// 复制节点
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
// 标记 | ShapeFlags.COMPONENT_KEPT_ALIVE,这个组件是复用的`VNode`, 这个标记 不走 mount逻辑
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
} else {
// 添加 pendingCacheKey
keys.add(key)
}
问题: 这里为什么不实现在
cache
中存入{pendingCacheKey: vnode}
呢?
答案: 这里其实可以加入这逻辑,只是官方间隔这个逻辑延后实现了, 我觉得没什么差别。
- 在组件挂载
onMounted
和更新onUpdated
的时候添加/更新缓存
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
const cacheSubtree = () => {
if (pendingCacheKey != null) {
// 添加/更新缓存
cache.set(pendingCacheKey, instance.subTree)
}
}
全部代码
const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,
setup(props: KeepAliveProps, { slots }: SetupContext) {
let current: VNode | null = null
// 缓存的一些数据
const cache = new Map()
const keys: Keys = new Set()
let pendingCacheKey: CacheKey | null = null
// 更新/添加缓存数据
const cacheSubtree = () => {
if (pendingCacheKey != null) {
// 添加/更新缓存
cache.set(pendingCacheKey, instance.subTree)
}
}
// 监听生命周期
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
return () => {
const children = slots.default()
const rawVNode = children[0]
// 获取缓存
const key = rawVNode.key
const cachedVNode = cache.get(key)
pendingCacheKey = key
if (cachedVNode) {
// 复用DOM和组件实例
rawVNode.el = cachedVNode.el
rawVNode.component = cachedVNode.component
} else {
// 添加 pendingCacheKey
keys.add(key)
}
rawVNode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = rawVNode
return rawVNode
}
}
}
至此,通过
cache
实现了DOM和组件实例的缓存。
keep-alive
的patch
复用逻辑
我们知道生成VNode
后是进行patch
逻辑,生成DOM
。
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
n2.slotScopeIds = slotScopeIds
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
}
}
processComponent
处理组件逻辑的时候如果是复用ShapeFlags.COMPONENT_KEPT_ALIVE
则走的父组件keep-alive
的activate
方法;
const unmount: UnmountFn = (
vnode,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false
) => {
const {
type,
props,
ref,
children,
dynamicChildren,
shapeFlag,
patchFlag,
dirs
} = vnode
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
return
}
}
unmount
卸载的keep-alive
组件ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
时调用父组件keep-alive
的deactivate
方法。
总结:
keep-alive
组件的复用和卸载被activate
方法和deactivate
方法接管了。
active
逻辑
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
const instance = vnode.component!
// 1. 直接挂载DOM
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// 2. 更新prop
patch(
instance.vnode,
vnode,
container,
anchor,
instance,
parentSuspense,
isSVG,
vnode.slotScopeIds,
optimized
)
// 3. 异步执行onVnodeMounted 钩子函数
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)
}
- 直接挂载DOM
- 更新prop
- 异步执行
onVnodeMounted
钩子函数
deactivate
逻辑
const storageContainer = createElement('div')
sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
// 1. 把DOM移除,挂载在一个新建的div下
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
// 2. 异步执行onVnodeUnmounted钩子函数
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)
}
}
- 把DOM移除,挂载在一个新建的div下
- 异步执行
onVnodeUnmounted
钩子函数
问题:旧节点的
deactivate
和新节点的active
谁先执行
答案:旧节点的deactivate
先执行,新节点的active
后执行。
keep-alive
的unmount
逻辑
- 将
cache
中出当前子树VNode节点外的所有卸载,当前组件取消keep-alive
的标记, 这样当前子树VNode会随着keep-alive
的卸载而卸载。
onBeforeUnmount(() => {
cache.forEach(cached => {
const { subTree, suspense } = instance
const vnode = getInnerChild(subTree)
if (cached.type === vnode.type) {
// 当然组件先取消`keep-alive`的标记,能正在执行unmout
resetShapeFlag(vnode)
// but invoke its deactivated hook here
const da = vnode.component!.da
da && queuePostRenderEffect(da, suspense)
return
}
// 每个缓存的VNode,执行unmount方法
unmount(cached)
})
})
<!-- 执行unmount -->
function unmount(vnode: VNode) {
// 取消`keep-alive`的标记,能正在执行unmout
resetShapeFlag(vnode)
// unmout
_unmount(vnode, instance, parentSuspense)
}
keep-alive
卸载了,其缓存的DOM
也将被卸载。
keep-alive
缓存的配置include
,exclude
和max
这部分知道逻辑就好了,不做代码分析。
- 组件名称在
include
中的组件会被缓存;- 组件名称在
exclude
中的组件不会被缓存;- 规定缓存的最大数量,如果超过了就把缓存的最前面的内容删除。
动态组件
使用方法
<keep-alive>
<component is="A"></component>
</keep-alive>
渲染函数
resolveDynamicComponent("A")
resolveDynamicComponent
的逻辑
export function resolveDynamicComponent(component: unknown): VNodeTypes {
if (isString(component)) {
return resolveAsset(COMPONENTS, component, false) || component
}
}
function resolveAsset(
type,
name,
warnMissing = true,
maybeSelfReference = false
) {
const res =
// local registration
// check instance[type] first which is resolved for options API
resolve(instance[type] || Component[type], name) ||
// global registration
resolve(instance.appContext[type], name)
return res
}
和指令一样,
resolveDynamicComponent
就是根据名称寻找局部或者全局注册的组件,然后渲染对应的组件。