Preface:

The main content of this article is from the following articles

  • Thoroughly uncovering the keep-alive principle github.com/qiudongwei/… (Main source of this article)

  • Vue source code parsing, keep-alive is how to implement cache? Juejin. Cn/post / 686220… (Main source of this article)

  • Keep alive – implementation principle of www.jianshu.com/p/9523bb439…

  • Blog. Myweb. Kim/vue/keep – al… blog.myweb.kim/vue/keep-alive/? utm-source=origin

  • Vue. Js, 7 ways to define the component template zhuanlan.zhihu.com/p/28073723

  • Understanding of the Vue component www.jianshu.com/p/ce97328a9…

  • Content introduction to Keep alive – principle and zhuanlan.zhihu.com/p/351499525 business solutions

Below is a personal summary of the above articles, with your own notes (bold and colored) for easy memorization.

When setting up a VUE project, some components don’t need to be rendered more than once, so they need to be ‘persisted’ in memory. You can keep the state of the contained component unchanged, even if the component is switched, its state remains in memory.

Vue keep-alive(1) : How does the vue router ensure that the page does not refresh when the page is reactivated

What keep-Alive is (Basic Concept presentation)

Keep-alive is an abstract component built into Vue.

What is a component

Another important concept of VUE is the component system, which is an abstraction that allows us to build large applications with small, independent, and often reusable components. Therefore, almost any type of application interface can be viewed as a component tree:

The function of a component is to transfer data from a parent scope to a child component, or to send data from within the component to the outside of the component, enabling data transfer to each other

Seven ways to define a component template

  1. String (String)

  2. Template literal

  3. X-Templates

  4. Inline

  5. Render functions

  6. JSX

  7. Single Page Components

This knowledge example is generally used with single-file components

Abstract component

Does not render in the DOM tree (real or virtual), does not render as a DOM element, and does not appear in the parent component chain — you will never find it in this.$parent

It has a property that abstract is true, indicating that it is an abstract component

Export default {name: 'abstractCompDemo', abstract: true, // mark as abstract component}Copy the code

I’m using this feature as a high-order HOC component in React.

How can abstract components ignore parent-child relationships

A Vue component calls initLifecycle during initialization, which determines whether the parent is an abstract component or not. If it is an abstract component, it selects the parent level above the abstract component, ignoring the hierarchy between the abstract component and its children.

/ / source location: SRC/core/instance/lifecycle. 32 lines of js export function initLifecycle (vm: Component) { const options = vm.$options // locate first non-abstract parent let parent = options.parent if (parent && ! options.abstract) { while (parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) } vm.$parent = parent // ... }Copy the code

If keep-alive has multiple child elements, keep-alive requires that only one child element be rendered at the same time.

How does the keep-alive component skip DOM generation?

Component instance parent-child relationships determine whether to ignore a component based on the abstract attribute. In keep-alive, if abstract: true is set, Vue skips the component instance.

The resulting component tree will not contain keep-alive components, so the DOM tree rendered by the component tree will not have keep-alive-related nodes.

Abstract component represents:

,, and other components

It’s just a matter of wrapping wrapped functionality around components, and it’s pretty comfortable to use. Throttling, anti-shaking, dragging, permission control, etc., can be encapsulated in this form.

Keep alive – components

** Keep-alive is an abstract component. ** When a dynamic component is wrapped with keep-alive, inactive component instances are cached rather than destroyed.

Keep-alive not only saves page/component states, but also improves system performance by avoiding repeated component creation and rendering. In general, keep-alive is used to save the render state of the component.

keep-alive props

  • Include defines a cache whitelist. Keep-alive caches hit components.

  • Exclude Specifies the blacklist. Matching components will not be cached.

  • Max defines the upper limit of the cache component, beyond which the cache data is replaced with LRU’s policy.

LRU is a page replacement algorithm for memory management.

LRU cache policy

Cache strategy: Replace the Least recently used data in memory with new data. Algorithms weed out data based on historical access records. The core idea is that if data has been accessed recently, it is more likely to be accessed in the future.

If a piece of data has not been accessed in the recent past, it is unlikely to be accessed in the future. That is, when the limited space is full of data, the data that has not been accessed for the longest should be eliminated.

The keep-alive cache mechanism sets the freshness of cached components according to the LRU policy, and removes components from the cache that have not been accessed for a long time.

Keep-alive source code analysis

Keep-alive. js also defines some utility functions inside, so let’s hold it down and look at the objects it exposes

// src/core/components/keep-alive.jsexport default { name: 'keep-alive', abstract: {include: patternTypes, // Cache whitelist exclude: patternTypes, // Cache blacklist Max: Created () {this.cache = object.create (null) // Cache this.keys = [] // cache key set}, Destroyed () {for (const key in this.cache) {pruneCacheEntry(this.cache, key, this.keys)}}, Mounted () {// Check (val); // Check (val); // Check (val); name)) }) this.$watch('exclude', val => { pruneCache(this, name => ! Matches (val, name))})}, render() {matches(val, name)}, render() { }}Copy the code

As you can see, as we defined the component, we first set the name of the component to keep-alive, and then define an abstract property with a value of true. This property is not mentioned in the official vue tutorial, but is crucial and will be used later in the rendering process.

Setting abstract to true at the beginning of the component indicates that the component is abstract. Abstract components, which deal only with wrapped child components, do not parent them, and do not render them as nodes on the page.

The props property defines all the parameters supported by the Keep-Alive component.

Keep-alive defines three hook functions during its lifetime:

created

Initialize two objects that cache vNodes (virtual DOM) and the corresponding key set of VNodes

destroyed

Delete the VNode instance cached in this.cache. We notice that instead of simply setting this.cache to null, we iterate over the pruneCacheEntry function to delete it.

// SRC /core/components/keep-alive.js 43 line function pruneCacheEntry (cache: VNodeCache, key: string, keys: Array<string>, current? : VNode) { const cached = cache[key] if (cached && (! current || cached.tag ! = = current. The tag)) {cached.com ponentInstance $destroyed () / / execute component destroy hooks} cache [key] = null remove (keys, key)}Copy the code

Deleting cached VNodes also requires the deStory hook function for the component instance

mounted

Mounted Listens for include and exclude arguments, and updates (deletes) the this.cache in real time. The core of the pruneCache function is also to call pruneCacheEntry

pruneCache
function pruneCache (keepAliveInstance: any, filter: Function) { const { cache, keys, _vnode } = keepAliveInstance for (const key in cache) { const cachedNode: ? VNode = cache[key] if (cachedNode) { const name: ? string = getComponentName(cachedNode.componentOptions) if (name && ! filter(name)) { pruneCacheEntry(cache, key, keys, _vnode) } } }}Copy the code
matches

To judge the cache rule, include and exclude can be learned. The value type can be string, regular expression, or array

function matches (pattern: string | RegExp | Array<string>, name: string): boolean {  if (Array.isArray(pattern)) {    return pattern.indexOf(name) > -1  } else if (typeof pattern === 'string') {    return pattern.split(',').indexOf(name) > -1  } else if (isRegExp(pattern)) {    return pattern.test(name)  }  return false}
Copy the code
pruneCacheEntry

PruneCacheEntry removes the component from the cache by calling the component $destroy method, emptying the cache component, and removing the corresponding key.

function pruneCache (keepAliveInstance: any, filter: Function) { const { cache, keys, _vnode } = keepAliveInstance for (const key in cache) { const cachedNode: ? VNode = cache[key] if (cachedNode) { const name: ? string = getComponentName(cachedNode.componentOptions) if (name && ! filter(name)) { pruneCacheEntry(cache, key, keys, _vnode) } } }}Copy the code

When keep-alive is mounted, it listens for include and exclude changes, adjusts the order of cache and keys when attributes change, and ultimately calls pruneCacheEntry.

render

Render is the core, so I leave it at the end. In short, keep-alive is the render function that determines the render result. 六四屠杀

GetFirstComponentChild retrieves the VNode of the first child. If keep-alive has more than one child, keep-Alive requires that only one child be rendered at the same time. So at the beginning we get the child element in the slot, and we call getFirstComponentChild to get the VNode of the first child element.

If the component name does not match include or exclude, it will exit directly and return to VNode without using the caching mechanism.

The matching condition goes into the logic of the cache mechanism. If the cache is hit, the cache instance is fetched from the cache and set to the current component, and the key position is adjusted to put it last (LRU policy). If not, cache the current VNode and add the key to the current component. If the number of cached components exceeds the value of Max, that is, the cache space is insufficient, then pruneCacheEntry is called to remove the oldest component from the cache, that is, the component of keys[0]. The component’s keepAlive flag is then set to true, indicating that it is cached.

render () { const slot = this.$slots.defalut const vnode: VNode = getFirstComponentChild(slot) const componentOptions: ?VNodeComponentOptions = vnode &&vnode.componentOptions if (componentOptions) {// Check pattern const name: ? String = getComponentName(componentOptions) const {include, exclude} = this if ( // not included (include && (! name || ! Matches (include, name))) | | / / excluded (exclude && name && matches (exclude, name))) {return vnode} const {cache, Keys} = this // define component cache key const key:? string = vnode.key === null ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : Vnode.key if (cache[key]) {vnode.componentInstance = cache[key]. ComponentInstance remove(keys, Key.push (key) if (this.max && keys.length >) else {cache[key] = vnode key.push (key) if (this.max && keys.length > ParseInt (this.max)) { Delete pruneCacheEntry(cahce, keys[0], keys, Enclosing _vnode)}} vnode. Data. KeepAlive = true / / rendering and execute wrapped component of hook function needed} return vnode | | (slot && slot [0])}Copy the code

Conclusion:

  1. Gets the first child component object wrapped around Keep-Alive and its component name;

  2. If keep-alive has multiple child elements, keep-alive requires that only one child element be rendered at the same time. So at the beginning we get the child element in the slot, and we call getFirstComponentChild to get the VNode of the first child element.

  3. According to the set blacklist and whitelist (if any) condition matching, decide whether to cache. If not, return the component instance (VNode), otherwise perform step 3.

  4. The cache Key is generated based on the component ID and tag, and the cache object is looked up to see if the component instance has been cached. If so, simply fetch the cached value and update the key’s position in this.keys (updating the key’s position is the key to implement the LRU substitution strategy), otherwise perform step 4.

  5. The this.cache object stores the component instance and the key, and then checks if the number of cached instances exceeds the Max value. If so, the most recent and oldest unused instance (the key with subscript 0) is deleted according to the LRU substitution policy.

  6. Last but not least, set the keepAlive property value of the component instance to true.

Vue rendering process in keep-live analysis

Here’s a look at the entire Vue rendering process:

In general

After the completion of VNode construction, it will eventually be converted into the real DOM, and patch is a necessary process.

The rendering of Vue starts from the Render phase in the diagram, but the rendering of Keep-Alive is in the patch phase, which is the process of building the component tree (virtual DOM tree) and converting vNodes into real DOM nodes.

Briefly describe the process from render to patch

Let’s start with the simplest new Vue:

import App from './App.vue'new Vue({render: h => h(App)}).$mount('#app')
Copy the code
  • Vue first calls the _render function on the prototype to convert the component object into a VNode instance; _render is converted by calling createElement and createEmptyVNode;

  • The createElement conversion process selects a new VNode or calls createComponent to instantiate the VNode, depending on the situation.

  • After the VNode is instantiated, Vue calls the _update function on the prototype to render the VNode into the real DOM. This is done by calling the patch function (patch phase).

Express it with a picture:

How does the keep-alive wrapped component use the cache?

During patch, the createComponent function is executed:

Function createComponent (vnode, insertedVnodeQueue, parentElm, RefElm) {let I = vnode.data if (isDef(I)) {// isReactivated indicates whether the component isReactivated. IsDef =(v)=>v! == undefined && v ! == null const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */) } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) {// initComponent assigns vnode.elm to the real dom initComponent(vnode, InsertedVnodeQueue) // insert inserts the component's real DOM into the parent element. insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } }}Copy the code
  1. The value of vnode.componentInstance is undefined and the value of keepAlive is true when the wrapped component is loaded for the first time. Therefore, isReactivated is false. Because the keep-alive component is the parent, its render function is executed before the wrapped component; I (vnode, false /* hydrating */);

  2. When the wrapped component is accessed again and the value of vnode.componentInstance is the cached component instance, the insert(parentElm, vnode.elm, refElm) logic is performed, which inserts the last DOM directly into the parent element.

The init function

The init function performs component initialization, which is a hook function of a component:

// SRC /core/vdom/create-component.jsconst componentVNodeHooks = {init (vnode: VNodeWithData, hydrating: Boolean): ? boolean { if ( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, // ... }Copy the code

CreateComponentInstanceForVnode will new component instance and assign values to the componentInstance Vue construction, then call $mount mount components.

reactivateComponent
function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i // hack for #4339: a reactivated component with inner transition // does not trigger because the inner node's created hooks are not called // again. It's not ideal to involve module-specific logic in here but // there doesn't seem to be a better way to do it.  let innerNode = vnode while (innerNode.componentInstance) { innerNode = innerNode.componentInstance._vnode if (isDef(i = innerNode.data) && isDef(i = i.transition)) { for (i = 0; i < cbs.activate.length; ++i) { cbs.activate[i](emptyNode, innerNode) } insertedVnodeQueue.push(innerNode) break } } // unlike a newly created component, // a reactivated keep-alive component doesn't insert itself insert(parentElm, vnode.elm, refElm) } function insert (parent, elm, ref) { if (isDef(parent)) { if (isDef(ref)) { if (nodeOps.parentNode(ref) === parent) { nodeOps.insertBefore(parent, elm, ref) } } else { nodeOps.appendChild(parent, elm) } } }Copy the code

So in the initial rendering, keep-alive caches the A component and renders the A component normally.

Cache rendering

When you switch to component B and then switch back to component A, component A’s hit cache is reactivated.

After going through the patch process again, keep-alive gets the current component according to the slot, so how does the content of the slot update the cache?

// SRC /core/vdom/patch. Js 714 line const isRealElement = isDef(oldvNode.nodeType)if (! isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)}Copy the code

During uninitialized rendering, Patch calls patchVnode to compare the old and new nodes.

// Source code location: src/core/vdom/patch.jsfunction patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) { // ... let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } // ...Copy the code

The hook function prepatch is called in patchVnode.

SRC /core/vdom/create-component.jsprepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { const options = vnode.componentOptions const child = vnode.componentInstance = oldVnode.componentInstance updateChildComponent( child, options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children )},Copy the code

The key update method is updateChildComponent, which updates the instance properties:

/ / source location: SRC/core/instance/lifecycle jsexport function updateChildComponent (vm: Component, propsData:? Object, listeners: ? Object, parentVnode: MountedComponentVNode, renderChildren: ? Array<VNode>) { // ... // Any static slot children from the parent may have changed during parent's // update. Dynamic scoped slots may also have changed. In such cases, a forced // update is necessary to ensure correctness. const needsForceUpdate = !! ( renderChildren || // has new static slots vm.$options._renderChildren || // has old static slots hasDynamicScopedSlot ) / /... // resolve slots + force update if has children if (needsForceUpdate) { vm.$slots = resolveSlots(renderChildren, parentVnode.context) vm.$forceUpdate() }}Vue.prototype.$forceUpdate = function () { const vm: Component = this if (vm._watcher) {// Vm._update (vm._render) vm._watcher.update()}Copy the code

NeedsForceUpdate is true only if there is a slot, and keep-alive is the condition. First call resolveSlots to update the slots for keep-alive, then call $forceUpdate to rerender keep-alive and render again. Since the A component is cached during initialization, keep-alive directly returns the cached A component VNode. After the VNode is ready, it comes to the patch phase.

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {  let i = vnode.data  if (isDef(i)) {    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive    if (isDef(i = i.hook) && isDef(i = i.init)) {      i(vnode, false /* hydrating */)    }    // after calling the init hook, if the vnode is a child component    // it should've created a child instance and mounted it. the child    // component also has set the placeholder vnode's elm.    // in that case we can just return the element and be done.    if (isDef(vnode.componentInstance)) {      initComponent(vnode, insertedVnodeQueue)      insert(parentElm, vnode.elm, refElm)      if (isTrue(isReactivated)) {        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)      }      return true    }  }}
Copy the code

The A component goes through createComponent again, calling init.

const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ? boolean { if ( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } },}Copy the code

Instead of following the logic of $mount, prepatch is called to update the instance properties. Created and Mounted lifecycle functions are not executed when the cache component is activated.

Return to createComponent where isReactivated is true and call reactivateComponent:

function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i // hack for #4339: a reactivated component with inner transition // does not trigger because the inner node's created hooks are not called // again. It's not ideal to involve module-specific logic in here but // there doesn't seem to be a better way to do it.  let innerNode = vnode while (innerNode.componentInstance) { innerNode = innerNode.componentInstance._vnode if (isDef(i = innerNode.data) && isDef(i = i.transition)) { for (i = 0; i < cbs.activate.length; ++i) { cbs.activate[i](emptyNode, innerNode) } insertedVnodeQueue.push(innerNode) break } } // unlike a newly created component, // a reactivated keep-alive component doesn't insert itself insert(parentElm, vnode.elm, refElm)}Copy the code

Finally, insert is called to insert the COMPONENT’s DOM node, and the cache rendering process is complete.

Keep-live hook function

For a keep-alive component, each load has a full life cycle, that is, the corresponding hook function within the life cycle will be triggered. Why not for a keep-alive component?

Execute hook functions only once

For a keep-alive component, each load has a full life cycle, that is, the corresponding hook function within the life cycle will be triggered. Why not for a keep-alive component?

This is because the cached component instance sets keepAlive = true, while in the initialization component hook function:

// src/core/vdom/create-component.jsconst componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ? boolean { if ( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } } // ... }Copy the code

When vnode.componentInstance and keepAlive are truly, $mount is not implemented. BeforeCreate, Created, and Mounted are not executed.

Reproducible Activated

At the end of the patch phase, the invokeInsertHook function invokes the component instance (VNode) ‘s own INSERT hook:

// src/core/vdom/patch.js function invokeInsertHook (vnode, queue, initial) { if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue } else { for (let i = 0; i < queue.length; ++ I) {queue[I].data.hook. Insert (queue[I]);Copy the code

Now look at the insert hook:

// src/core/vdom/create-component.jsconst componentVNodeHooks = { // init() insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (! componentInstance._isMounted) { componentInstance._isMounted = true callHook(componentInstance, 'mounted') } if (vnode.data.keepAlive) { if (context._isMounted) { queueActivatedComponent(componentInstance) } else { activateChildComponent(componentInstance, true /* direct */) } } // ... }Copy the code

Inside the hook, the activateChildComponent function is called to recursively execute the activated hook function of all the child components:

// src/core/instance/lifecycle.jsexport function activateChildComponent (vm: Component, direct? : boolean) { if (direct) { vm._directInactive = false if (isInInactiveTree(vm)) { return } } else if (vm._directInactive) { return } if (vm._inactive || vm._inactive === null) { vm._inactive = false for (let i = 0; i < vm.$children.length; i++) { activateChildComponent(vm.$children[i]) } callHook(vm, 'activated') }}Copy the code

Conversely, the Deactivated hook function works the same way. The deactivateChildComponent is called from the Destroy hook function of a component instance (VNode).