Keep alive component is a component provided by Vue. It can cache component instances and avoid component mounting and unloading in some cases. It is very practical in some scenarios.
For example, we recently encountered a scenario in which a component uploads a large file is a time-consuming operation. If you switch to other page contents during uploading, the component will be unloaded and the corresponding download will be cancelled. At this time, you can wrap this component with the keep alive component. When you switch to other pages, the component can still continue to upload files, and you can also see the upload progress when you switch back.
keep-alive
Render child nodes
const KeepAliveImpl: ComponentOptions = { name: `KeepAlive`, setup(props: KeepAliveProps, { slots }: SetupContext) { // Subtree VNode to render let current: VNode | null = null return () => { // Get the child node. Because keep alive can only have one child node, directly take the first child node const children = slots.default() const rawVNode = children[0] // Tag | shapeflags COMPONENT_ SHOULD_ KEEP_ Alive, this component is a 'keep alive' component. This tag does not follow unmount logic because it needs to be cached vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE // Record current child node current = vnode // Returns a child node that represents the rendering of this child node return rawVNode } } }
The setup return function of the component, which is the rendering function of the component;
Keep alive is a virtual node. You don't need to render, you only need to render the child node, so the function only needs to return the child node VNode.
Cache function
- Define the Map for storing cached data. All cache key array Keys represent the cache key value pendingCacheKey of the current sub component;
const cache = new Map() const keys: Keys = new Set() let pendingCacheKey: CacheKey | null = null
- Get the key of the sub tree node VNode in the rendering function, and check whether there is a cache node corresponding to the key in the cache
const key = vnode.key const cachedVNode = cache.get(key)
key is added when generating the rendering function of child nodes. Generally, it is 0, 1, 2.
- Record the key before the point
pendingCacheKey = key
- If the cached cachedVNode node is found, copy the component instances and node elements of the cached cachedVNode node to the new VNode node. If not found, first add the pendingCacheKey of the current subtree node VNode to the Keys.
if (cachedVNode) { // Replication node vnode.el = cachedVNode.el vnode.component = cachedVNode.component // Tag | shapeflags COMPONENT_ KEPT_ Alive, this component is a reusable 'VNode', and this tag does not follow the mount logic vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE } else { // Add pendingCacheKey keys.add(key) }
Question: why not store {pendingCacheKey: vnode} in the cache?
Answer: in fact, this logic can be added here, but the logic of official interval is delayed. I don't think it makes any difference.
- Add / update the cache when the component mounts onMounted and updates onUpdated
onMounted(cacheSubtree) onUpdated(cacheSubtree) const cacheSubtree = () => { if (pendingCacheKey != null) { // Add / update cache cache.set(pendingCacheKey, instance.subTree) } }
All codes
const KeepAliveImpl: ComponentOptions = { name: `KeepAlive`, setup(props: KeepAliveProps, { slots }: SetupContext) { let current: VNode | null = null // Some cached data const cache = new Map() const keys: Keys = new Set() let pendingCacheKey: CacheKey | null = null // Update / add cached data const cacheSubtree = () => { if (pendingCacheKey != null) { // Add / update cache cache.set(pendingCacheKey, instance.subTree) } } // Listening lifecycle onMounted(cacheSubtree) onUpdated(cacheSubtree) return () => { const children = slots.default() const rawVNode = children[0] // Get cache const key = rawVNode.key const cachedVNode = cache.get(key) pendingCacheKey = key if (cachedVNode) { // Reuse DOM and component instances rawVNode.el = cachedVNode.el rawVNode.component = cachedVNode.component } else { // Add pendingCacheKey keys.add(key) } rawVNode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE current = rawVNode return rawVNode } } }
So far, the caching of DOM and component instances is realized through cache.
Keep alive patch reuse logic
We know that after generating VNode, patch logic is used to generate 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 ) } } }
If shapeflags are reused when processComponent processes component logic COMPONENT_ KEPT_ Alive follows the activate method of the parent component keep alive;
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 unmounted keep alive component shapeflags COMPONENT_ SHOULD_ KEEP_ When alive, call the deactivate method of the parent component keep alive.
Summary: the reuse and unloading of keep alive components are taken over by the activate method and the deactivate method.
active logic
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => { const instance = vnode.component! // 1. Mount DOM directly move(vnode, container, anchor, MoveType.ENTER, parentSuspense) // 2. Update prop patch( instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, vnode.slotScopeIds, optimized ) // 3. Execute onVnodeMounted hook function asynchronously 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) }
- Mount DOM directly
- Update prop
- Execute onVnodeMounted hook function asynchronously
deactivate logic
const storageContainer = createElement('div') sharedContext.deactivate = (vnode: VNode) => { const instance = vnode.component! // 1. Remove the DOM and mount it under a new div move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense) // 2. Execute onVnodeUnmounted hook function asynchronously 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) } }
- Remove the DOM and mount it under a new div
- Execute onVnodeUnmounted hook function asynchronously
Question: who will execute the deactivate of the old node and the active of the new node first
Answer: the deactivate of the old node is executed first, and the active of the new node is executed later.
unmount logic of keep alive
- Unload all nodes except the current subtree VNode node in the cache, and the current component cancels the mark of keep alive, so that the current subtree VNode will be unloaded with keep alive.
onBeforeUnmount(() => { cache.forEach(cached => { const { subTree, suspense } = instance const vnode = getInnerChild(subTree) if (cached.type === vnode.type) { // Of course, the component first cancels the mark of 'keep alive', which can be unmount resetShapeFlag(vnode) // but invoke its deactivated hook here const da = vnode.component!.da da && queuePostRenderEffect(da, suspense) return } // Execute unmount method for each cached VNode unmount(cached) }) }) <!-- implement unmount --> function unmount(vnode: VNode) { // Cancel the mark of 'keep alive' to unmount the execution resetShapeFlag(vnode) // unmout _unmount(vnode, instance, parentSuspense) }
Keep alive is unloaded, and its cached DOM will also be unloaded.
The configuration of keep alive cache includes, excludes and max
It's good to know the logic in this part, without code analysis.
- Components with component names in include will be cached;
- Components with component names in exclude will not be cached;
- Specify the maximum number of caches. If it exceeds, delete the first content of the cache.
Dynamic component
usage method
<keep-alive> <component is="A"></component> </keep-alive>
Rendering function
resolveDynamicComponent("A")
Logic of 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 }
Like the directive, resolvedynamic component is to find the locally or globally registered component according to its name, and then render the corresponding component.