[Vue source code] Logical analysis of keep-alive components (Part 1)

Start the growth journey of Nuggets! This is the 15th day of my participation in the “Nuggets Daily New Project · February Update Challenge”, click to view the event details

keep-alive

Basic usage

The use of keep only needs to add labels to the outermost layer of dynamic components

<div id="app">
    <button @click="changeTabs('child1')">child1</button>
    <button @click="changeTabs('child2')">child2</button>
    <keep-alive>
        <component :is="chooseTabs">
        </component>
    </keep-alive>
</div>
var child1 = {<!-- -->
    template: '<div><button @click="add">add</button><p>{<!-- -->{num}}</p></div>',
    data() {<!-- -->
        return {<!-- -->
            num: 1
        }
    },
    methods: {<!-- -->
        add() {<!-- -->
            this.num++
        }
    },
}
var child2 = {<!-- -->
    template: '<div>child2</div>'
}
var vm = new Vue({<!-- -->
    el: '#app',
    components: {<!-- -->
        child1,
        child2,
    },
    data() {<!-- -->
        return {<!-- -->
            chooseTabs: 'child1',
        }
    },
    methods: {<!-- -->
        changeTabs(tab) {<!-- -->
            this.chooseTabs = tabs;
        }
    }
})

When the dynamic component switches back and forth between child1 and child2, after the second switch to the child1 component, child1 retains the original data state.

Compile from template to virtual DOM

There is no difference between built-in components and ordinary components in the compilation process. No matter which component or user-defined component, the components are processed in the same way when the template is compiled into a render function. The generation process of the render function is not analyzed here.

After getting the render function, start to generate the virtual DOM. Since keep-alive is a component, it will call the createComponent function to create the virtual DOM of the sub-component. In the link of createComponent, the difference from creating a normal component is that the keep-alive Virtual DOM will remove redundant attributes, except for the slot attribute (slot attribute is also deprecated after version 2.6), other attributes are meaningless. On the virtual DOM of the keep-alive component, there is an abstract attribute as a sign of the abstract component.

// Create subcomponent Vnode process
function createComponent(Ctordata,context,children,tag) {<!-- -->
  // abstract is the sign of built-in components (abstract components)
  if (isTrue(Ctor. options. abstract)) {<!-- -->
    // Only the slot attribute is retained, other label attributes are removed, and no longer exist on the vnode object
    var slot = data. slot;
    data = {<!-- -->};
    if (slot) {<!-- -->
        data.slot = slot;
    }
  }
}

Initial rendering

The reason why keep-alive is special is that it will not render the same component repeatedly, but will only use the cache reserved for the first rendering to update nodes, in order to understand keep-alive, we start to analyze his first rendering.

Process Analysis

The same as ordinary components, Vue will get the virtual DOM object generated earlier, and execute the process of creating real nodes, which is the process of patch , in the process of creating a node, the virtual DOM of keep-alive will be considered as a vnode of a component, so it will enter createComponent function in which the keep-alive component is initialized and instantiated.

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {<!-- -->
  let i = vnode.data
  if (isDef(i)) {<!-- -->
    // isReactivated is used to determine whether the component is cached
    const isReactivated = isDef(vnode.componentInstance) & amp; & amp; i.keepAlive
    if (isDef(i = i.hook) & amp; & amp; isDef(i = i.init)) {<!-- -->
      // Internal hook to perform component initialization init
      i(vnode, false /* hydrating */)
    }
    if (isDef(vnode. componentInstance)) {<!-- -->
      // One of the functions is to keep the real DOM in the virtual DOM
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {<!-- -->
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

The keep-alive component will first call the internal init hook for initialization,

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 {<!-- -->
      // Copy the component instance to the componentInstance property of the virtual DOM
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      // Mount after creating the component instance
      child.$mount(hydrating? vnode.elm : undefined, hydrating)
    }
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {<!-- -->
    //...
  },

  insert (vnode: MountedComponentVNode) {<!-- -->
    //...
  },

  destroy (vnode: MountedComponentVNode) {<!-- -->
    //...
  }
}

In the first execution, there is no componentInstance attribute in the component’s vnode object, and vnode.data.keepAlive has no value, so it will call createComponentInstanceForVnode method instantiates the component and copies the component instance to the componentInstance property of vnode, and finally executes the $mount method of the component instance to mount the instance.

createComponentInstanceForVnode is the process of component instantiation. This process has been analyzed before, mainly including initialization operations such as option merger, initialization event, and life cycle.

Built-in component options

When using components, often define component options in the form of objects, including data , method , computed , etc., and in the parent component or global Register to see the specific options of keep-alive

export default {<!-- -->
  name: 'keep-alive',
  abstract: true,
  // props values allowed by the keep-alive component
  props: {<!-- -->
    include: patternTypes, // Component names that need to be cached
    exclude: patternTypes, // Component names that do not need to be cached
    max: [String, Number] // The maximum number of caches, when this number is exceeded, it will be replaced according to the lru algorithm
  },

  created () {<!-- -->
    // cache component vnode
    this.cache = Object.create(null)
    // Cache component name
    this.keys = []
  },

  destroyed () {<!-- -->
    for (const key in this.cache) {<!-- -->
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {<!-- -->
    // For dynamic include and exclude, need to monitor
    this.$watch('include', val => {<!-- -->
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {<!-- -->
      pruneCache(this, name => !matches(val, name))
    })
  },

  // render function
  render () {<!-- -->
    // Get the value of the slot under keep-alive
    const slots = this. $slots. default
    // Get the first vnode node
    const vnode: VNode = getFirstComponentChild(slot)
    // get the first component instance
    const componentOptions: ?VNodeComponentOptions = vnode & amp; & amp; vnode.componentOptions
    if (componentOptions) {<!-- -->
      // check pattern
      // Get the name attribute of the first subcomponent vnode
      const name: ?string = getComponentName(componentOptions)
      const {<!-- --> include, exclude } = this
      if (
        // Determine whether the subcomponent needs to be cached, and return the vnode object directly if no cache is required
        // not included
        (include & amp; & amp; (!name || !matches(include, name))) ||
        //excluded
        (exclude & amp; & amp; name & amp; & amp; matches(exclude, name))
      ) {<!-- -->
        return vnode
      }

      const {<!-- --> cache, keys } = this
      const key: ?string = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ?componentOptions.Ctor.cid + (componentOptions.tag ? `::${<!-- -->componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) {<!-- -->
        // Hit the cache,
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        // After deleting the key, add it again. The most recently used cache will be placed behind the array. This is the idea of the lru algorithm, which will be analyzed in detail later
        remove(keys, key)
        keys. push(key)
      } else {<!-- -->
        // first render, cache vnode
        cache[key] = vnode
        keys. push(key)
        // prune oldest entry
        if (this.max & amp; & amp; keys.length > parseInt(this.max)) {<!-- -->
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      // Add flags to the cache component
      vnode.data.keepAlive = true
    }
    return vnode || (slot & amp; & amp; slot[0])
  }
}

keep-alive components are similar to normal components in terms of options, keep-alive components use render function instead of template template. The keep-alive component is essentially just a process of storing and fetching caches, without actual node rendering.

Cache VNode

After the keep-alive component is instantiated, the component will be mounted, and the mounting process will return to the process of vm._render and vm_update . Since keep-alive has render function, we directly analyze the implementation of render function

Get the content of the keep-alive component slot

The first is to obtain the content of the slot under keep-alive through the getFirstComponentChild method, which is the subcomponent that the keep-alive component needs to render.

export function getFirstComponentChild (children: ?Array<VNode>): ?VNode {<!-- -->
  if (Array.isArray(children)) {<!-- -->
    for (let i = 0; i < children. length; i ++ ) {<!-- -->
      const c = children[i]
      // If the component instance exists, return it, theoretically return the vnode of the first component
      if (isDef(c) & amp; & amp; (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {<!-- -->
        return c
      }
    }
  }
}

Cache component

After getting the subcomponent instance, you need to judge whether the matching conditions of the cache are met. The matching conditions can use arrays, strings, and regular expressions.

const name: ?string = getComponentName(componentOptions)
const {<!-- --> include, exclude } = this
if (
  // Determine whether the subcomponent needs to be cached, and return the vnode object directly if no cache is required
  // not included
  (include & amp; & amp; (!name || !matches(include, name))) ||
  //excluded
  (exclude & amp; & amp; name & amp; & amp; matches(exclude, name))
) {<!-- -->
  return vnode
}

function matches (pattern: string | RegExp | Array<string>, name: string): boolean {<!-- -->
  // allows the use of arrays
  if (Array.isArray(pattern)) {<!-- -->
    return pattern. indexOf(name) > -1
  } else if (typeof pattern === 'string') {<!-- -->
    // allows the use of strings
    return pattern.split(',').indexOf(name) > -1
  } else if (isRegExp(pattern)) {<!-- -->
    // Allow regular forms
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}

If the component does not meet the cache requirements, it will return the vnode of the component, and then enter the mounting process

The key step of the render function is to cache the vnode. Since the render function is executed for the first time, the cache and keys has no data and cannot hit the cache. Therefore, when the keep-alive component is rendered for the first time, the subcomponent vnode that needs to be rendered will be rendered > for caching. Mark the cached vnode and return the vnode of the subcomponent, vnode.data.keepAlive = true

Saving of real nodes

Going back to the logic of crateComponent, in the createComponent method, the initialization process of the keep-alive component will be executed first, including the subcomponents Mount, and then get the keep-alive component instance in the createComponent method, the next important step is to save the real DOM in the createComponent code>vnode.

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {<!-- -->
  let i = vnode.data
  if (isDef(i)) {<!-- -->
    // isReactivated is used to determine whether the component has a cache
    const isReactivated = isDef(vnode.componentInstance) & amp; & amp; i.keepAlive
    if (isDef(i = i.hook) & amp; & amp; isDef(i = i.init)) {<!-- -->
      // Internal hook to perform component initialization init
      i(vnode, false /* hydrating */)
    }
    if (isDef(vnode. componentInstance)) {<!-- -->
      // One of the functions is to keep the real DOM in the virtual DOM
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {<!-- -->
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

function initComponent (vnode, insertedVnodeQueue) {<!-- -->
  if (isDef(vnode.data.pendingInsert)) {<!-- -->
    insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
    vnode.data.pendingInsert = null
  }
  // Save the real DOM to vnode
  vnode.elm = vnode.componentInstance.$el
  if (isPatchable(vnode)) {<!-- -->
    invokeCreateHooks(vnode, insertedVnodeQueue)
    setScope(vnode)
  } else {<!-- -->
    // empty component root.
    // skip all element-related modules except for ref (#3455)
    registerRef(vnode)
    // make sure to invoke the insert hook
    insertedVnodeQueue.push(vnode)
  }
}

When performing component caching, it is necessary to save the real DOM node of the component to the vnode object, and saving a large number of DOM elements will consume a lot of performance , so we need to strictly control the number of cache components, and also need to optimize the cache strategy.

A summary of the first rendering of the keep-alive component: the built-in keep-alive component, when the subcomponent is rendered for the first time, the vnode cached with the real DOM