preface
Knowing what it is and what it is, a good engineer must not only be adept at using the framework, but also understand how it is implemented underneath. This paper mainly explores the implementation principle of KeepAlive component built in Vue3 source code.
KeepAlive is an abstract component that does not render a real DOM, but only the inner child nodes, and optimizes performance by keeping the inner child components from going through a whole recursive process of unloading and mounting the DOM as they switch. If you want to know how to use Vue3 KeepAlive, it is described in detail on the website.
Realize the principle of
KeepAlive component in the source code is an object, its implementation is mainly component rendering, component cache processing, props three parameters processing and component uninstall process.
// packages/runtime-core/src/components/KeepAlive.ts
const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`.// Three parameters received by the keep-alive component
props: {
include: [String.RegExp.Array].exclude: [String.RegExp.Array].max: [String.Number]},...setup(props: KeepAliveProps, { slots }: SetupContext) {
// Component uninstallation logic.// returns the render function
return () = >{...// Cache logic processing
// props parameter processing logic
// Component initialization logic}}}Copy the code
In the example above, when the setup function returns a function, that function is the component’s rendering function.
The component rendering
Look directly at the following source code:
// packages/runtime-core/src/components/KeepAlive.ts
const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`.setup(props: KeepAliveProps, { slots }: SetupContext){...// returns the render function
return () = > {
pendingCacheKey = null
if(! slots.default) {return null
}
// Get the children element wrapped around keep-alive
const children = slots.default()
const rawVNode = children[0]
if (children.length > 1) {
// Keep-alive renders only a single child node, with an error greater than 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)) ) {SUSPENSE is returned if it is not a vNode or custom component
current = null
return rawVNode
}
...
current = vnode
return rawVNode
}
}
}
Copy the code
As you can see from above, the vNode rendered by KeepAlive is the first element of the children node, which is the return value of the function. So we say that KeepAlive is an abstract component that does not render itself as a solid node, but as its first child node.
Cache handling
The KeepAlive component caches the DOM, because DOM rendering is a patch recursive process and is the most performance-consuming. The KeepAlive component injects two hook functions, onMounted and onUpdated. The cacheSubtree function is executed inside both of these hook functions to cache:
.// packages/runtime-core/src/components/KeepAlive.ts
const cacheSubtree = () = > {
// fix #1621, the pendingCacheKey could be 0
if(pendingCacheKey ! =null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
Copy the code
PendingCacheKey is only assigned in KeepAlive’s render function, so KeepAlive’s onMounted hook is not cached for the first time. PendingCacheKey is then assigned to vNode.key when KeepAlive executes render.
So when a component switch triggers a re-rendering of the component, which in turn triggers a re-rendering of the KeepAlvie component. Before re-rendering of the component, the onUpdate hook function is executed, and the cacheSubtree function is executed again.
PendingCacheKey corresponds to the vNode key of A, and instance.subTree corresponds to the render subTree of A, so KeepAlive caches the render subTree of the previous component before updating.
When we switch back to the original component, the KeepAlvie component is rerendered again, and the onUpdate hook function is used to cache the B component’s render subtree.
CachedVNode (cachedVNode); render (KeepAlive); render (KeepAlive); render (KeepAlive);
if (cachedVNode) {
// 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 the vNode being mounted as a new node
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// Keep the key fresh
keys.delete(key)
keys.add(key)
} else {
keys.add(key)
// Delete the oldest key
if (max && keys.size > parseInt(max as string.10)) {
pruneCacheEntry(keys.values().next().value)
}
}
Copy the code
Once we have the cached render subtree, we can directly take its corresponding DOM and component instance, assign it to KeepAlive vNode, and update vNode.shapeFlag for subsequent patch phase. Patch is implemented by the processComponent method.
// packages/runtime-core/src/renderer.ts
const processComponent = (n1: VNode | null,n2: VNode, ...) = > {
n2.slotScopeIds = slotScopeIds
if (n1 == null) {
/ / processing keep alive
if(n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { ; (parentComponent! .ctxas KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
// Mount the componentmountComponent(n2,...) }}else {
updateComponent(n1, n2, optimized)
}
}
Copy the code
KeepAlive renders a child node for the first time just like a normal component node. However, with caching, the processComponent function executes the logic that handles KeepAlive components because it marks shapeFlag. Execute the activate function in the KeepAlive context. Let’s see how it works:
// packages/runtime-core/src/components/KeepAlive.ts
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) = > {
const instance = vnode.component!
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// in case props have changed
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)
}
}
Copy the code
As you can see, since you can get the cached DOM from vnode.el, you can mount the node directly by calling the move method and then executing the patch method to update the component to prevent the props from changing. The next step is to execute the Activated hook function defined by the child node component after the component is rendered, via queuePostRenderEffect.
Props parameter processing
KeepAlive supports three Props, including, Exclude, and Max.
// packages/runtime-core/src/components/KeepAlive.ts
props: {
include: [String.RegExp.Array].exclude: [String.RegExp.Array].max: [String.Number]}Copy the code
Include and exclude correspond to the following implementation logic:
// packages/runtime-core/src/components/KeepAlive.ts
const { include, exclude, max } = props
if( (include && (! name || ! matches(include, name))) || (exclude && name && matches(exclude, name)) ) { current = vnodereturn rawVNode
}
Copy the code
Vnodes whose subcomponent names do not match include and whose subcomponent names match exclude should not be cached and should be returned.
Because the props are responsive, include and exclude props should also have some processing logic when they change as follows:
// packages/runtime-core/src/components/KeepAlive.ts
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})Copy the code
The logic for listening is simple. When the include changes, remove vNodes whose names do not match the include from the cache. When the exclude changes, remove vNodes whose names match the exclude from the cache.
In addition to include and exclude, the KeepAlive component also supports Max Prop to control the maximum number of caches.
// packages/runtime-core/src/components/KeepAlive.ts
if (cachedVNode) {
...
} else {
keys.add(key)
// prune oldest entry
if (max && keys.size > parseInt(max as string.10)) {
pruneCacheEntry(keys.values().next().value)
}
}
Copy the code
Since new cache keys are added at the end of keys, when the number of caches exceeds Max, they are removed from the front.
Component uninstall
The unmount method is used to unmount the component, which has a logic for the KeepAlive component as follows:
// packages/runtime-core/src/renderer.ts
const unmount: UnmountFn = (vnode,parentComponent,parentSuspense,doRemove = false,optimized = false) = >{...if(shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { ; (parentComponent! .ctxas KeepAliveContext).deactivate(vnode)
return}... }Copy the code
If shapeFlag satisfies the KeepAlive condition, the corresponding deactivate function is executed, which is defined as follows:
// packages/runtime-core/src/components/KeepAlive.ts
sharedContext.deactivate = (vnode: VNode) = > {
const instance = vnode.component!
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)
}
}
Copy the code
The function first removes the node from the DOM tree using the move method, and then executes the defined DeActivated hook function using queuePostRenderEffect.
When the KeepAlive component is uninstalled, the KeepAlive component is uninstalled due to the recursive nature of uninstallation. The onBeforeUnmount hook function is executed during the uninstallation as follows:
// packages/runtime-core/src/components/KeepAlive.ts
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
constda = vnode.component! .da da && queuePostRenderEffect(da, suspense)return
}
unmount(cached)
})
})
Copy the code
It iterates over all cached VNodes and verifies that the cached vNode is not the one currently rendered by the KeepAlive component.
If so, the resetShapeFlag method is used to change the shapeFlag of the VNode so that it is no longer treated as a KeepAlive Vnode, so that normal uninstallation logic can be followed. The deactivated hook function of the child component is then executed via queuePostRenderEffect.
If not, the unmount method is used to reset the shapeFlag and perform the entire uninstallation process of the cached VNode.
Write in the last
Finally, the implementation principle of KeepAlive component is summarized:
KeepAlive
The vNode rendered by the component is the first element of the children node, which is the return value of the function.KeepAlive
The component injects two hook functions, onMounted and onUpdated, and executes the cacheSubtree function inside both of them for caching. The hook function executes before the Render function, so the last component is cached during the switch, and then the Patch procedure calls activate to mount it directly using the cache vNode.- KeepAlive supports three Props, including, Exclude, and Max. Vnodes whose subcomponent names do not match include and whose subcomponent names match exclude should not be cached and should be returned. Cache keys are added at the end of keys, so when the number of caches exceeds Max, they are removed from the front.
- This method calls KeepAlive deactivate directly to remove the DOM. When the KeepAlive component is unmounted, the KeepAlive component will be unmounted due to the recursive nature of the unmount. The onBeforeUnmount hook function is executed during the unmount cycle to unload the node.
PS: To quickly build your own front-end static blog, check out Vuepress Quick Build Blog – a blog topic you deserve.
Reference documentation
Vue. Js 3.0 core source code internal reference