is a built-in component in Vue source code. It is defined in SRC /core/components/keep-alive.js:

export default {
  name: 'keep-alive'.abstract: true.props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String.Number]
  },
  created () {},
  destroyed () {},
  mounted () {},
  render () {}
}
Copy the code

Created, Mounted, destroyed lifecycle functions, and custom render functions are created, created, created, destroyed lifecycle functions, and created. Include, exclude, and Max are the components that need to be cached, the components that do not need to be cached, and the maximum length of the cache list.

Let’s start with the whole process of the following demo

import Vue from 'vue'
/ / component A
const A = {
  name: 'A'.template: '
      
This is a
, created () { console.log('A executes created') }, mounted () { console.log('A/mounted') }, activated () { console.log('A executes activated') }, deactivated () { console.log('A deactivated')}}/ / component B const B = { name: 'B'.template: '
This is B
, created () { console.log('B executes created') }, mounted () { console.log('B 'mounted) }, activated () { console.log('B executes activated') }, deactivated () { console.log('B deactivated')}}new Vue({ components: { A, B }, el: '#app'.template: '
< button@click ="change"> click // click toggle component
`
, data () { return { com: 'A'}},methods: { change () { this.com = this.com === 'A' ? 'B' : 'A'}}})Copy the code

Two components, A and B, are defined, with four life cycles each. The root component references these two components and wraps them in the

component. Let’s look at the flow

First apply colours to a drawing

First create the root component instance and the Render Watcher of the root component. Then execute the Render function of the root component to generate a VNode. When a Keep-alive component is encountered, a component VNode is created for keep-alive. And create the components of A VNode principle said in an article this way (slot), A component of A component VNode added to keep alive – component VNode componentOptions. Children. Next, perform the patch process for the root component and create a Vue instance for the Keep-Alive component. During instance creation, the initLifecycle method is called, which has such logic

export function initLifecycle (vm: Component) {
  const options = vm.$options
  // The options of the child Vue instance will have the parent property, which is the value of the previous Vue instance
  let parent = options.parent
  if(parent && ! options.abstract) {while(parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) } vm.$parent = parent  vm.$root = parent ? parent.$root : vm// ...
  
}
Copy the code

The main purpose of this logic is to parent component instances. If options.abstract is not true for the current component instance, add the current component instance to $children for the parent instance of options.abstract that is not true

  • Vue instance of the root componentvm.$parentfornull.
  • Due to thekeep-aliveComponent definitionabstractfortrueDoes not add its own Vue instance to the parent instance$childrenIn, but its own Vue instance$parentProperty still points to the parent instance. And the subcomponents (AWhen creating a Vue instance, it adds itself to thekeep-aliveOf the parent instance of the component instance$childrenIn the

$parent of keep-alive Vue refers to the root instance, and $children of keep-alive Vue is null. $parent of the Vue instance of component A points to the root instance; The root instance’s $parent points to null, and the root instance’s $children contains the Vue instance of component A

The Created lifecycle of Keep-Alive is then called

created () {
  this.cache = Object.create(null)
  this.keys = []
}
Copy the code

Keep-alive defines this.cache and this.keys in the created life cycle, which are used to store cached components

After the keep-alive Vue instance is created, the keep-Alive component’s custom render function is executed to create a render VNode.

render () {
  const slot = this.$slots.default // Get the slot content
  const vnode: VNode = getFirstComponentChild(slot)
  constcomponentOptions: ? VNodeComponentOptions = vnode && vnode.componentOptionsif (componentOptions) {
    // Gets the name of the current component, or the label name if the name attribute is not set
    constname: ? string = getComponentName(componentOptions)const { include, exclude } = this
    if( (include && (! name || ! matches(include, name))) || (exclude && name && matches(exclude, name)) ) {return vnode
    }

    const { cache, keys } = this
    const key = vnode.key == null
      ? componentOptions.Ctor.cid + (componentOptions.tag ? ` : :${componentOptions.tag}` : ' ')
    : vnode.key
    if (cache[key]) {
      vnode.componentInstance = cache[key].componentInstance
      remove(keys, key)
      keys.push(key)
    } else {
      cache[key] = vnode
      keys.push(key)
      if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
      }
    }
    vnode.data.keepAlive = true
  }
  return vnode || (slot && slot[0])}Copy the code

First get the slot content with this.$slot.default; GetFirstComponentChild getFirstComponentChild getFirstComponentChild getFirstComponentChild getFirstComponentChild Obtain componentOptions based on component VNode of component A.

The matches method matches the name of the current component to include, exclude, or tag if the name attribute is not set

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

The matches method shows that include and exclude can be arrays, comma-delimited strings, and regular expressions

If the current component name is not included in include or exclude, the VNode of the current component is returned. Otherwise, the caching process is performed

The caching process is as follows

render () {
  const slot = this.$slots.default // Get the slot content
  const vnode: VNode = getFirstComponentChild(slot)
  constcomponentOptions: ? VNodeComponentOptions = vnode && vnode.componentOptionsif (componentOptions) {
    // ...

    const { cache, keys } = this
    const key = vnode.key == null
      ? componentOptions.Ctor.cid + (componentOptions.tag ? ` : :${componentOptions.tag}` : ' ')
    : vnode.key
    if (cache[key]) {
      vnode.componentInstance = cache[key].componentInstance
      remove(keys, key)
      keys.push(key)
    } else {
      cache[key] = vnode
      keys.push(key)
      if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
      }
    }
    vnode.data.keepAlive = true
  }
  return vnode || (slot && slot[0])}Copy the code

Create a key variable. If vnode.key is not null, the value of the key is vNode. key, otherwise the value is a concatenated string. If not, store the current VNode in cache, and store the key in keys. If the Max passed in is less than the length of the keys, the pruneCacheEntry method is called to remove the VNode corresponding to the first element in the keys array. The purpose is to clear the cache.

function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>, current? : VNode) {
  const cached = cache[key]
  // Cached has value and is not the component currently being rendered
  if(cached && (! current || cached.tag ! == current.tag)) { cached.componentInstance.$destroy() } cache[key] =null
  remove(keys, key)
}
Copy the code

The pruneCacheEntry method retrieves the VNode in the cache based on the passed key. If the VNode is not the VNode being rendered, the VNode instance’s $destroy() method is called to unload the component. This is an LRU caching strategy. The VNode corresponding to the first element of the keys array can be understood as the least used VNode. If the render function hits the cache, the key is removed from the keys and added back to the end.

Go back to the render function, set vnode.data.keepAlive = true after the cache is added, and return to vnode; This VNode is the VNode of component A in demo. Then enter the keep-alive patch process, during which the Vue instance of component A will be created and mounted to vnode.componentInstance, and then the rendering VNode of component A will be created. When the whole DOM tree is created and inserted into the page, Insert hook functions collected during the patch process are executed, including component A’s INSERT hook function

insert (vnode: MountedComponentVNode) {
 // vnode.context points to the Vue instance where the component vNode was created
  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

KeepAlive (vnode.data.keepAlive) is true, but the mounted life cycle of the parent vnode has not yet been triggered. So instead of calling the queueActivatedComponent method, call the activateChildComponent(componentInstance, True) method

export 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

The activateChildComponent method has the following logic

  • ifdirectfortrueTo set upvm._directInactive = false; callisInInactiveTreeMethod to determine all ancestor instances of the component instance passed in_inactiveIs the propertytrue.
    • If I have a thetatrue, indicating that this ancestral instance is also wrapped inkeep-aliveInside and not active directlyreturn.That is, if the parent of the current instance is not active, the current instance will not be calledactivatedThe life cycle.
    • Whereas if all the grandparent instances_inactiveProperties forfalseOr without this property, to the componentAAdd an instance of_inactiveProperty, set tofalseIndicates that this component is active. Then traverseAAll descendant instances of the component instance, if anykeep-aliveTriggers the corresponding descendant componentactivatedLife cycle, and then trigger its ownactivatedLife cycle, soactivatedThe order of execution is child before parent
  • ifdirectforfalseandvm._directInactivefortrue, return directly

Render the B component for the first time

The change method is called when the button in the demo is clicked

change () {
  this.com = this.com === 'A' ? 'B' : 'A'
}
Copy the code

Take a look at the overall update process, modifying the com properties and putting Watcher updates from the root component into the next queue via nextTick. The component VNode of component B is created during the rendering VNode of the root component. After the patch process of the root component is performed, the prepatch hook function of keep-alive is called when the keep-alive is a component, and the updateChildComponent method is executed. And call keep-alive’s $forceUpdate method to update. Specific process in Vue source code (ten) slot principle. Call the keep-alive watcher.run method, execute the render function, and return component VNode of component B; Then the keep-alive patch process is entered. During the patch process, oldVnode is the component VNode of component A, while VNode is the component VNode of component B. Since the two component VNodes are different, a Vue instance of component B is created and the mount process of component B is performed

When the rendering VNode of component B is mounted, the patch method of keep-alive will be returned, and there is such logic in the patch method

// destroy old node
if (isDef(parentElm)) {
  removeVnodes([oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
  invokeDestroyHook(oldVnode)
}
Copy the code

Both components A and B are on the page, so component A’s destroy hook function is called

destroy (vnode: MountedComponentVNode) {
  const { componentInstance } = vnode
  if(! componentInstance._isDestroyed) {if(! vnode.data.keepAlive) { componentInstance.$destroy() }else {
      deactivateChildComponent(componentInstance, true /* direct */)}}}Copy the code

Because component A’s vNode.data. keepAlive is true, the deactivateChildComponent method is called instead of unloading component A

export function deactivateChildComponent (vm: Component, direct? : boolean) {
  if (direct) {
    vm._directInactive = true
    if (isInInactiveTree(vm)) {
      return}}if(! vm._inactive) { vm._inactive =true
    // Triggers the deactivated hook of the Keep-alive component in all child VMS when it becomes inactive
    for (let i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i])
    }
    callHook(vm, 'deactivated')}}Copy the code

Sets the _directInactive property to true and iterates to see if _inactive of the ancestor instance is true, that is, inactivated, and if so returns directly; Otherwise, if the component instance’s _inactive property is false, it is active. Set the component instance’s _inactive property to True. If the descendant component has keep-alive and the current status is active, the deactivated life cycle of the descendant component is invoked, and then the deactivated life cycle of the descendant component is invoked.

When the entire DOM tree is created and mounted, component B’s INSERT method is called

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

The queueActivatedComponent method is executed because the parent component (here also the root component, because component B’s component VNode is also created in the root component) has already been mounted (the Mounted life cycle was performed during the first creation)

export function queueActivatedComponent (vm: Component) {
  vm._inactive = false
  activatedChildren.push(vm)
}
Copy the code

The queueActivatedComponent method sets the _inactive of the instance to false to indicate the active component and adds the component to the activatedChildren. If the parent component contains keep-alive and mounted is executed, the value of activated is not triggered when the insert hook of the parent component is called. Instead, you add the Vue instance of the wrapped component to activatedChildren, as explained below.

FlushSchedulerQueue will loop through all the watchers in the queue and call watcher.run. In flushSchedulerQueue, we will return to this method to continue execution

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  queue.sort((a, b) = > a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    if(process.env.NODE_ENV ! = ='production'&& has[id] ! =null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn()
        break}}}// Start here
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  // Reset all states
  resetSchedulerState()

  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
}
Copy the code

Call the callActivatedHooks method and pass in the activatedQueue, which holds all the descendant keep-Alive wrapped component instances

function callActivatedHooks (queue) {
  for (let i = 0; i < queue.length; i++) {
    queue[i]._inactive = true
    activateChildComponent(queue[i], true /* true */)}}Copy the code

Iterate over all instances, set _inactive to true, and then call activateChildComponent to invoke the Activated life cycle for all instances

Mounted = mounted = mounted = mounted = mounted = mounted = mounted

This is because there is a judgment in the activateChildComponent method that isInInactiveTree(VM), if you do not set the _inactive property of the parent component instance wrapped by the Keep-Alive component to false, Returns the Activated life cycle function directly instead of executing it.

Cache components

When the button is clicked again to trigger the update, the component VNode of component A has been cached. Obtain the Vue instance from the cached component VNode and mount the instance to the component VNode of component A. Keys are then updated and the corresponding key for component A is added back to the end of keys

render () {
  const slot = this.$slots.default
  const vnode: VNode = getFirstComponentChild(slot)
  constcomponentOptions: ? VNodeComponentOptions = vnode && vnode.componentOptionsif (componentOptions) {
    constname: ? string = getComponentName(componentOptions)const { include, exclude } = this
    if( (include && (! name || ! matches(include, name))) || (exclude && name && matches(exclude, name)) ) {return vnode
    }

    const { cache, keys } = this
    constkey: ? string = vnode.key ==null
      ? componentOptions.Ctor.cid + (componentOptions.tag ? ` : :${componentOptions.tag}` : ' ')
    : vnode.key
    if (cache[key]) {
      vnode.componentInstance = cache[key].componentInstance
      remove(keys, key)
      keys.push(key)
    } else {
      cache[key] = vnode
      keys.push(key)
      if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
      }
    }
    vnode.data.keepAlive = true
  }
  return vnode || (slot && slot[0])}Copy the code

After returning to the Vnode, the keep-alive patch process is entered. When a component Vnode is encountered, the createComponent method is called

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    // Check whether the component instance already exists and the component is wrapped with keep-alive
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      // Execute the component's init hook function
      i(vnode, false /* hydrating */)}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’s init hook function is called

init (vnode: VNodeWithData, hydrating: boolean): ? boolean {if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) {const mountedNode: any = vnode
    componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
    const child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode,
      activeInstance
    )
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
},
Copy the code

At this point, if is true, so would call A component prepatch hook function, rather than using createComponentInstanceForVnode recreate the Vue instance, so will not call created life cycle, and will not perform mount process again

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = vnode.componentOptions
  const child = vnode.componentInstance = oldVnode.componentInstance
  updateChildComponent(
    child,
    options.propsData, // Updated props Specifies the latest props value for the child component
    options.listeners, // Updated listeners define events
    vnode, // new parent vnode
    options.children // Latest slot VNode)},Copy the code

Call the updateChildComponent method in prepatch. Since keep-alive is essentially A slot, needsForceUpdate is true and $forceUpdate is called to update component A

export function updateChildComponent (
  vm: Component, // Subcomponent instance
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode, / / component vnode
  renderChildren: ?Array<VNode>
) {
  const newScopedSlots = parentVnode.data.scopedSlots
  const oldScopedSlots = vm.$scopedSlots
  consthasDynamicScopedSlot = !! ( (newScopedSlots && ! newScopedSlots.$stable) || (oldScopedSlots ! == emptyObject && ! oldScopedSlots.$stable) || (newScopedSlots && vm.$scopedSlots.$key ! == newScopedSlots.$key) )constneedsForceUpdate = !! ( renderChildren ||// has new static slots
    vm.$options._renderChildren ||  // has old static slots
    hasDynamicScopedSlot
  )
  if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }
}
Copy the code

After component A is updated, go back to createComponent and insert DOM into the target position through insert method. InitComponent is called to update the DOM structure of the VNode. Since isReactivated is true, the reactivateComponent method is also executed. And then we do an insert, which is kind of weird, and I don’t know why we’re doing an insert

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

After the insertion is complete, the remaining logic is the same as the second click above. At the end of the keep-alive patch method, component B is deleted and the deactivated life cycle function is called. The mounted lifecycle is not executed because component A has already been mounted. Instead, add the Vue instance to activatedChildren and set the _inactive property to false. When all components are rendered, go back to the flushSchedulerQueue and call the callActivatedHooks method to trigger the Activated life cycle function for all elements in activatedChildren.

conclusion

Keep-alive the way slots are used; When the parent component VNode is created, the component VNode is also created for the wrapped component. When the keep-alive render function is called, get the default slot VNode and check if the component VNode is cached:

  • If it has been cached, the component VNode is given a cached Vue instance to prevent it from being created again. That is to say,keep-aliveThe Vue instance of the component is cached, and after each hit, the Vue instance does not need to be created again. Instead, the previously cached instance is used. Finally, the component VNode is returned.
  • If not, add the cache using the LRU cache policy. Finally, the component VNode is returned.