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 oneinactiveTags 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:

  1. Gets the renderer method
  2. Creating a hidden container
  3. Add to the shared context objectactivateanddeactivatehook
  4. Determine whether child components meet caching requirements
  5. Get cached content/add cached content
  6. 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 objectactivateanddeactivatehook

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

  • includeUse to display the component whose configuration should be cached
  • excludeUsed 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