I’m participating in nuggets Creators Camp # 4, click here to learn more and learn together!
preface
In our daily development, we often use the functions of cache routing or components, such as switching between two components; The keep-alive component can be used when you jump from a list page to a detail page and return with the status of the last operation on the list page (paging, search criteria, etc.).
The design idea of KeepAlive component actually originates from the KeepAlive of HTTP protocol: to avoid the extra performance cost of frequently creating and destroying HTTP connections. In VUE, KeepAlive is used to cache components, not only to avoid frequent creation and destruction of components, but also to remember the state of components (generally used for caching routes).
The basic use
We define components test-a and test-b respectively, hereinafter referred to as component A and component B for ease of writing:
test-a
<template>
<div @click="increment()">i am test a, number: {{ number }}</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const number = ref(0)
function increment() {
number.value++
}
</script>
Copy the code
This is a simple accumulator component that increments the number by one by clicking the text section
test-b
<template>
<div>i am test b</div>
</template>
Copy the code
Then import these two files in index.vue:
<script lang="ts" setup> import { ref } from 'vue' import TestA from '.. /components/test-a.vue' import TestB from '.. /components/test-b.vue' const flag = ref(true) </script> <template> <keep-alive> <test-a v-if="flag"></test-a> <test-b v-else></test-b> </keep-alive> <button @click="flag = ! </button> </template>Copy the code
Note: KeepAlive can only cache component-type nodes; non-components are rendered directly. Ensure that only one subcomponent exists at a time.
The v-if function should be obvious: it removes components with a value of false from the DOM tree entirely
Click test-a twice first to change the value of number to 2. Click the switch button and then switch back. It can be found that the number in test-a will not be cleared with the switch.
When we open Vue devTools, we can clearly see that after we switch from component A to component B, component A is not destroyed, but is typed with oneinactive
Tags and vice versa:
That’s what KeepAlive is for: it caches the entire state of a component to avoid repeated creation and destruction
Realize the principle of
The implementation of KeepAlive is simple:
Before we unload the KEEP-Alive wrapped A component, we move the A component from its original position to a hidden container, and when it needs to be remounted, we move the A component from the hidden container to its original position. I drew a picture to make it a little bit clearer:
Source code analysis
KeepAlive source address, note: this article is based on latest (2022/2/27) code, version v3.2.31
I comb the main process of the source code, can be divided into six stages:
- Gets the renderer method
- Creating a hidden container
- Add to the shared context object
activate
anddeactivate
hook - Determine whether child components meet caching requirements
- Get cached content/add cached content
- Rendering component
0. Get renderer methods
const instance = getCurrentInstance()
const parentSuspense = instance.suspense
const sharedContext = instance.ctx
const { renderer: { p: patch, m: move, um: _unmount, o: { createElement } } } = sharedContext
Copy the code
The KeepAlive component communicates with the renderer through instance. CTX, the context object of the renderer instance. Here we mainly use four methods: get update patch, move, unmount _unmount, and create createElement
1. Create a hidden container
Then create a hidden container with the createElement method taken from the renderer
const storageContainer = createElement('div')
Copy the code
2. Add the shared context objectactivate
anddeactivate
hook
Activate and deactivate hooks are unique to KeepAlive components. They are triggered when a KeepAlive component is activated and deactivated, respectively, to avoid repeated calls to mountComponent to mount and unmount cached components
activate
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
const instance = vnode.component
move(vnode, container, anchor, 0 /* ENTER */, parentSuspense)
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)
}
Copy the code
Activate moves the cached component node from the hidden node to the original container and marks the component instance’s isDeactivated property to false in the render queue. After the move function, the update method patch is called:
const patch(n1, n2, container, achor) { if (shapeFlag & ShapeFlags.COMPONENT) { if (n1 == null) { if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { parentComponent.ctx.activate(n2, container, anchor, parentComponent, ParentSuspense)} else {// mountComponent mountComponent(n2, container, anchor, parentComponent, parentSuspense)}}}}Copy the code
You can see that if a node type has a COMPONENT_KEPT_ALIVE representation, the renderer does not remount it, but instead calls Activate to activate itself
deactivate
sharedContext.deactivate = (vnode) => {
const instance = vnode.component
move(vnode, storageContainer, null, 1 /* 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)
}
Copy the code
When deactivate is triggered, the cached component nodes are moved from parentSuspense to hidden storageContainer and the component instance isDeactivated flag is set to true
3. Check whether subcomponents meet cache requirements
KeepAlive works only if the node to be cached is a component node
The KeepAlive component does not return null
if (! slots.default) { return null }Copy the code
If there is more than one KeepAlive subcomponent, a warning is thrown in the production environment and a list of subcomponents is returned
const children = slots.default()
const rawVNode = children[0]
if (children.length > 1) {
if (__DEV__) {
warn(`KeepAlive should contain exactly one component child.`)
}
current = null
return children
}
Copy the code
When there is no component node in KeepAlive, the native node is returned
if ( ! isVNode(rawVNode) || (! (rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) && ! (rawVNode.shapeFlag & ShapeFlags.SUSPENSE)) ) { current = null return rawVNode }Copy the code
After several layers of filtering, we can cache the child components that really need to be cached:
4. Obtain or add cache content
Create a cache object cache
const cache = new Map()
Copy the code
Also create a key with no duplicate values, which is designed specifically for KeepAlive caching, so that each child node has a unique key
const keys = new Set()
Copy the code
Create a pendingCacheKey to cache subtree components after rendering
let pendingCacheKey = null
Copy the code
Determine if there is anything in the cache that needs to be mounted before mounting the node
const key = vnode.key == null ? comp : vnode.key
const cachedVNode = cache.get(key)
Copy the code
Also assign pendingCacheKey to key to prepare for caching for this instance
pendingCacheKey = key
Copy the code
If yes, no mounting is required
If (cachedVNode) {// Inherits the cached component instance vnode.el = cachedvNode. el vnode.ponent = cachedvNode.ponent // Animate if (vnode.transition) {setTransitionHooks(vnode, vnode.transition!) } // Change the shapeFlag type to COMPONENT_KEPT_ALIVE, Avoid node is used as a new node mount vnode. ShapeFlag | = ShapeFlags.COM PONENT_KEPT_ALIVE / / that the key value of the latest, this step is to cache management, Key.delete (key) key.add (key)}Copy the code
If not, add the component’s key to keys
Else {keys.add(key) // If the Max attribute is passed into the component and the number of cached components exceeds Max, If (Max && keys.size > parseInt(Max, 10)) {pruneCacheEntry(keys.values().next().value)}}Copy the code
The function that removes the oldest cached instance:
Function pruneCacheEntry(key: CacheKey) {const cached = cache.get(key) // Unmount an instance that does not exist on the current page. current || cached.type ! == current. Type) {unmount(cached) // If you delete an instance of the current page, it should not be cached. Elseif (current) {resetShapeFlag(current) // current.shapeflag -= ShapeFlags.COMPONENT_KEPT_ALIVE } cache.delete(key) keys.delete(key) }Copy the code
Remember pendingCacheKey above? OnMounted saves the component instances that need to be cached in the cache during the onMounted phase of the lifecycle:
const cacheSubtree = () => { if (pendingCacheKey ! = null) { cache.set(pendingCacheKey, GetInnerChild (instance.subtree))}} onMounted(cacheSubtree) onUpdated(cacheSubtree)Copy the code
5. Render the node
Finally, the node type is added, and then the first child component in KeepAlive is rendered
Vnode. ShapeFlag | = ShapeFlags.COM PONENT_SHOULD_KEEP_ALIVE current = vnode / / can flip up, Const rawVNode = children[0] return rawVNodeCopy the code
So KeepAlive can be considered a virtual component because it does not actually exist in the DOM tree and returns its first child instance.
Principle of cache
By default, all subcomponents contained with KeepAlive are cached, and performance problems are inevitable when there are too many cached components. We can use the Max attribute to specify the maximum number of caches.
Remember when you deleted and added a key in order to determine that a cache instance already exists?
keys.delete(key)
keys.add(key)
Copy the code
Also, when a Max attribute is passed in to a component and the number of cached components exceeds Max, the oldest cached instance is removed
if (max && keys.size > parseInt(max, 10)) {
pruneCacheEntry(keys.values().next().value)
}
Copy the code
This is the LRU(Least Recently Used) algorithm for KeepAlive.
The principle is relatively simple: set a limited capacity to fill the cache, when the capacity is full, the earliest cache is deleted, to make room for the latest cache.
The implementation in KeepAlive looks like this. I drew a flow chart:
include & exclude
include
Use to display the component whose configuration should be cachedexclude
Used to show components whose configuration should not be cached
When we configure these two attributes, it will determine before generating the cache instance:
if( (include && (! name || ! matches(include, name))) || (exclude && name && matches(exclude, name)) ) { current = vnodereturn rawVNode
}
Copy the code
If the component name does not match the include value/re, or if it matches the exclude value/re, the native node is returned
reference
- Vue.js design and implementation [Huo Chunyang] (currently the best Vue3 source/principle/design ideas interpretation of the book, no one)
- KeepAlive源码 core/KeepAlive.ts at main · vuejs/core
- Renderer. Ts at main · vuejs/core