解读Vue3中keep-alive和动态组件的实现逻辑
作者:JonnyLan
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就是根据名称寻找局部或者全局注册的组件,然后渲染对应的组件。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。