This is the 30th day of my participation in the Wenwen Challenge
Vue3 source code series of articles in serial…
In daily development, if we need to save the state of a component when it switches, and prevent it from being destroyed and rendered multiple times, we usually use
component processing, because it can cache inactive components, rather than destroy them. At the same time, the
component does not render its own DOM elements and does not appear in the parent chain of the component, belonging to an abstract component. When a component is switched within
, its activated and deactivated hook functions are executed accordingly.
Basic usage
Here is an example use of the
component,
<keep-alive :include="['a', 'b']" :max="10">
<component :is="view"></component>
</keep-alive>
Copy the code
Properties Props
-
Include A string or expression. Only components with matching names are cached.
-
Exclude A string or regular expression. Components with matching task names are not cached.
-
Max number. Maximum number of component instances can be cached.
Note that the
component is used when immediate child components are switched on. If there are multiple conditional child elements, only one element can be rendered at the same time.
Component source code implementation
We have seen the
component’s definition, properties, and usage. Let’s take a look at how the source code is implemented.
Abstract component
Let’s cut out the extra code and see how the KeepAlive component is defined.
const KeepAliveImpl = { __isKeepAlive: true, inheritRef: true, props: { include: [String, RegExp, Array], exclude: [String, RegExp, Array], Max: [String, Number]}, setup(props: KeepAliveProps, {slots}: SetupContext){// return()=>{ if (! Slot.default) {return null} // Get the child of the component const children = slot.default () // get the first child let vnode = children[0] // If (children. Length > 1) {current = null return children} else if (! isVNode(vnode) || ! (vnode.shapeFlag&shapeflags.stateful_component) {current = null return vnode} // Omit other code... Return vnode}}}Copy the code
The KeepAlive component is implemented through the Composition API, and setup returns the component’s rendering function. In a rendering function, the KeepAlive component takes the child nodes of the component. If there are more than one child node, the KeepAlive component is returned to all of the nodes. When only one child exists, rendering the contents of the first child verifies that KeepAlive is an abstract component and does not render its own DOM elements.
Caching mechanisms
Before we look at the KeepAlive component caching mechanism, let’s look at the concept of the LRU algorithm, which is used to handle the caching mechanism.
LRU algorithm
We often use cache to increase data queried. Due to the limited cache capacity, when the cache capacity reaches the upper limit, it is necessary to delete some data to make space for new data to be added. Therefore, some policies are needed to manage the data that is added to the cache. Common strategies are:
-
LUR has not been used for the longest time
-
FIFO first in first out
-
NRU Clock replacement algorithm
-
The LFU uses the substitution algorithm at least
-
PBA page buffering algorithm
The KeepAlive cache mechanism uses the LRU algorithm (Least Recently Used). If data has been accessed in the recent period, it will be accessed frequently in the future. This means that if frequently accessed data, we need to be able to hit it quickly, and infrequently accessed data, we are out of capacity and want to weed it out.
We are only talking about the concept here, if you want to understand the LRU algorithm in depth, you can look it up.
Cache implementation
Simplify the code, get rid of the core code, and look at the caching mechanism
Const KeepAliveImpl = {setup(props){// Cache a KeepAlive child's data structure {key:vNode} const cache: Cache = new Map() const keys: keys = new Set() let current: VNode | null = null let pendingCacheKey: CacheKey | null = null / / in beforeMount/Update cache subtree const cacheSubtree = () = > {the if (pendingCacheKey! = null) { cache.set(pendingCacheKey, instance.subTree) } } onBeforeMount(cacheSubtree) onBeforeUpdate(cacheSubtree) return ()=>{ pendingCacheKey = null const children = slots.default() let vnode = children[0] const comp = vnode.type as Component const name = getName(comp) // Const {include, exclude, Max} = props // The key was added when the KeepAlive child was created, Const key = vnode.key == null? Comp: Const cachedVNode = cache.get(key) if (cachedVNode) {// Cache exists, Vnode. el = cachedvNode. el vnode. ponent = cachedvNode. ponent if (vnode.transition) {// Update the subtree recursion transition hooks setTransitionHooks(vnode, vnode.transition!) } / / stop vNode node as the new node is mount vNode. ShapeFlag | = ShapeFlags.COM PONENT_KEPT_ALIVE / / let the key always fresh keys. Delete (key) keys. The add (key)} Add (key) else {keys.add(key) // Set Max value, delete the most unused key, If (Max && keys.size > parseInt(Max as string, (10)) {pruneCacheEntry keys. The values (). The next () value)}} / / avoid vNode uninstalled vNode. ShapeFlag | = ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE current = vnode return vnode; }}}Copy the code
KeepAlive declares a cache variable to cache node data. It is a Map structure. LRU cache algorithm is adopted to deal with the child node storage mechanism, as follows:
-
Declare an ordered collection keys as a cache container that uniquely identifies the key of the cache component
-
Keys Cache the data in the container. The earlier the key value, the less it is accessed, the older the value, and the fresher the value
-
When rendering function is executed, if the cache is hit, delete the current hit key from the keys, and append the key value to the end of keys, save fresh
-
If the cache is not hit, keys will append the cache data key value. If the cache data length is greater than the Max value, the oldest data will be deleted. The value here is the first value in keys, which is consistent with LRU idea.
-
Caches data for the currently active subtree when the beforeMount/ Update life cycle is triggered
Mount the difference
In general, component mounts and unmounts trigger their own life cycles. Is there a difference between a KeepAlive subtree and a cache mount phase? Take a look at the core code related to ShapeFlags.COMPONENT type in the next patch phase.
const processComponent = (n1: VNode | null,n2: VNode,container: RendererElement,anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null,parentSuspense: SuspenseBoundary | null, isSVG: boolean,optimized: Boolean) => {if (n1 == null) {// if (n2.shapeflags.com PONENT_KEPT_ALIVE) {// if (n2.shapeflags.com PONENT_KEPT_ALIVE) { (parentComponent! . CTX as KeepAliveContext). Activate (n2, container, anchor, isSVG, optimized)} else {/ / otherwise, Mount components mountComponent (n2, container, anchor, parentComponent parentSuspense, isSVG, optimized)}} else {/ / update the components updateComponent(n1, n2, optimized) } }Copy the code
KeepAlive components when rendering function, if any cache, can give a vNode give vNode. ShapeFlag | = ShapeFlags.COM PONENT_KEPT_ALIVE state, thus rendering the subtree again, will perform parentComponent! The.ctx.activate function activates the state of the subtree. So what’s the activate function here? Look at the code
const instance = getCurrentInstance() const sharedContext = instance.ctx as KeepAliveContext sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => { const instance = vnode.component! // Mount node move(vnode, container, Anchor, MoveType.ENTER, parentSuspense) There may be a props change patch (instance. Vnode vnode, container, anchor, the instance, parentSuspense, isSVG, optimized) QueuePostRenderEffect (() => {// Once the component is rendered, Execute the actived hook function instance.isDeactivated = false if (instance.a) {invokeArrayFns(instance.a)} const vnodeHook = vnode.props && vnode.props.onVnodeMounted if (vnodeHook) { invokeVNodeHook(vnodeHook, instance.parent, vnode) } }, parentSuspense) }Copy the code
When the subtree is activated again, since the vNode was cached in the last rendering, you can retrieve the cached DOM directly from the vNode, and there is no need to go through the vNode again. So move can mount the subtree directly, then patch can update the component, and finally queuePostRenderEffect can execute the Activate hook function defined by the child node component after the component is rendered.
KeepAlive can communicate with a KeepAlive instance by passing the renderer inside the KeepAlive instance’s CTX property, and exposing the activate/deactivate implementation through KeepAlive. This is done to avoid importing KeepAlive directly into the renderer to generate tree-shaking.
Properties for
KeepAlive supports three attributes include, exclude, and Max. Here’s the implementation of the other two properties, which Max has covered above.
setup(){ watch( () => [props.include, props.exclude], ([include, exclude]) => { include && pruneCache(name => matches(include, name)) exclude && pruneCache(name => matches(exclude, name)) } ) return ()=>{ if ( (include && (! name || ! matches(include, name))) || (exclude && name && matches(exclude, name)) ) { return (current = vnode) } } }Copy the code
If the child component name does not match the value of include, or if the child component name matches the value of exclude, it should not be cached. The watch function listens to changes in include and exclude values and reacts accordingly, that is, deletes the corresponding cached data.
Uninstall process
The uninstallation process is divided into the uninstallation process caused by the subcomponent switchover and the uninstallation process caused by the KeepAlive component uninstallation.
Child components working process and unloading process component will execute unmount to approach, then execute the parentComponent. CTX. Deactivate (vnode) function, through the move function to remove node in a function, The defined DeActivated hook function is then executed using queuePostRenderEffect. This process is similar to the mount process, but is described in more detail.
When a KeepAlive component is unmounted, the onBeforeUnmount function is triggered. Now look at the implementation of this function:
onBeforeUnmount(() => { cache.forEach(cached => { const { subTree, suspense } = instance if (cached.type === subTree.type) { resetShapeFlag(subTree) const da = subTree.component! .da da && queuePostRenderEffect(da, suspense) return } unmount(cached) }) })Copy the code
When the cached VNode is a vnode rendered by the current KeepAlive component, reset the ShapeFlag of the vnode so that it is not regarded as a KeepAlive VNode, and then execute the deactivated function of the child component through queuePostRenderEffect. This completes the unload logic. Otherwise, run the unmount method to perform the entire vNode unmount process.
Attached: LRU algorithm
Class LRUCache {constructor (capacity) {enclosing capacity = capacity | | 2. This cache = new Map ()} / / stock value, beyond the default to delete the first largest: The least recently used element put(key,val){if(this.cache.has(key)){this.cache.delete(key)} if(this.cache.size>=this.capacity){ This.cache.delete (this.cache.keys().next().value)} this.cache.set(key,val)} Get (key){if(this.cache.has(key)){const temp = this.cache.get(key) this.cache.delete(key) this.cache.set(key,temp) return temp } return -1 } }Copy the code
conclusion
At this point we have explored how the KeepAlive component is designed as an abstract component, how the related properties are implemented, and the LRU caching mechanism.