preface

The most common approach to performance optimization is caching. Caching frequently accessed resources reduces the time or memory consumption by reducing requests or initialization. Vue provides us with a cache component, keep-Alive, which can be used for routing level or component-level caching.

But do you know how caching works and how component cache rendering works? So this article will analyze the principle of keep-alive.

LRU strategy

When using keep-alive, you can add prop attributes include, exclude, and Max to allow conditional caching of components. Since there are constraints, old components need to be removed from the cache, and new components need to be added to the latest cache, how to formulate the corresponding policy?

The Least recently used LRU policy filters out data based on historical data access records. The DESIGN principle of the LRU policy is that if a piece of data has not been accessed in the recent past, it is highly unlikely that it will 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.

  1. Now the cache allows only 3 components at most, and ABC components enter the cache one by one without any problems
  2. When component D is accessed, the memory space is insufficient. A is the earliest and oldest component to enter, so component A is removed from the cache and component D is added to the latest location
  3. When component B is accessed again, since B is still in the cache, B moves to the latest location, with the other components one bit later
  4. When E component is accessed, the memory space is insufficient, C becomes the oldest unused component, C component is removed from the cache, and E component is added to the latest location

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. Now that you understand the caching mechanism, go into the source code to see how the Keep-alive component is implemented.

Component Implementation Principle

SRC /core/components/keep-alive.js
export default {
  name: 'keep-alive'.abstract: true.props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String.Number]
  },
  created () {
    this.cache = Object.create(null)
    this.keys = []
  },
  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },
  mounted () {
    this.$watch('include'.val= > {
      pruneCache(this.name= > matches(val, name))
    })
    this.$watch('exclude'.val= > {
      pruneCache(this.name= >! matches(val, name)) }) }, render () {const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    constcomponentOptions: ? VNodeComponentOptions = vnode && vnode.componentOptionsif (componentOptions) {
      // check pattern
      constname: ? 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
      constkey: ? 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]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      } else {
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        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

Kepp-alive is actually an abstract component that only deals with wrapped child components. It does not have a parent-child relationship with the child components and does not render them as nodes on the page. Setting abstract to true at the beginning of the component indicates that the component is abstract.

/ / source location: SRC/core/instance/lifecycle. 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

So how does an abstract component ignore this relationship? During initialization, initLifecycle is called to determine if the parent is an abstract component, and if it is, the parent level above the abstract component is chosen, ignoring the hierarchical relationship between the abstract component and its children.

Back to the keep-alive component, instead of writing a template, the render function determines the render result.

const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
Copy the code

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.

// check pattern
constname: ? string = getComponentName(componentOptions)const { include, exclude } = this
if (
  // not included(include && (! name || ! matches(include, name))) ||// excluded
  (exclude && name && matches(exclude, name))
) {
  return vnode
}

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

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

const { cache, keys } = this
constkey: ? 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]) {
  vnode.componentInstance = cache[key].componentInstance
  // make current key freshest
  remove(keys, key)
  keys.push(key)
} else {
  cache[key] = vnode
  keys.push(key)
  // prune oldest entry
  if (this.max && keys.length > parseInt(this.max)) {
    pruneCacheEntry(cache, keys[0], keys, this._vnode)
  }
}
vnode.data.keepAlive = true
Copy the code

The matching condition goes into the cache mechanism’s logic. 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. 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.

function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>, current? : VNode) {
  const cached = cache[key]
  if(cached && (! current || cached.tag ! == current.tag)) { cached.componentInstance.$destroy() } cache[key] =null
  remove(keys, key)
}
Copy the code

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

mounted () {
  this.$watch('include'.val= > {
    pruneCache(this.name= > matches(val, name))
  })
  this.$watch('exclude'.val= > {
    pruneCache(this.name= >! matches(val, name)) }) }function pruneCache (keepAliveInstance: any, filter: Function) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    constcachedNode: ? VNode = cache[key]if (cachedNode) {
      constname: ? 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.

Summary: Cache is used to cache components, keys store components’ keys, and cache components are adjusted according to THE LRU policy. Keep-alive’s render returns the VNode of the component at the end, so we can also conclude that keep-alive does not actually render, but that the rendered object is a child of the wrapped component.

Component rendering process

Warm tips: This part needs to have an understanding of the render and patch process

The main two processes in the rendering process are render and patch. Before render, there will be template compilation. The render function is the product of template compilation, which is responsible for building the VNode tree, and the constructed VNode will be passed to patch. Patch generates a real DOM node tree according to the relationship between VNodes.

This diagram describes the flow of Vue view rendering:

After the completion of VNode construction, it will eventually be converted into the real DOM, and patch is a necessary process. To better understand the process of component rendering, it is assumed that keep-Alive includes two components, A and B, and A is displayed by default.

Initialize render

CreateComponent is executed to mount the component during the patch process, and component A is no exception.

// source: SRC /core/vdom/patch.js
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

IsReactivated indicates whether a component isReactivated. At the time of initial rendering, A component has not been initialized, so componentInstance is undefined. The keepAlive of A component is true, because keep-alive, as the parent wrapped component, will be mounted before A component, that is, kepp-alive will perform the render process first, and A component is cached. After that, the keepAlive value of the first component in the slot (A component) is set to true. Therefore, isReactivated is false.

The component is then initialized by calling init, which is a hook function of the component:

SRC /core/vdom/create-component.js
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

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

Back to createComponent, continue with the following logic:

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

Call initComponent to assign vNode. elm to the real DOM, and then call INSERT to insert the component’s real DOM into the parent element.

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?

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: SRC /core/vdom/patch.js
function 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.js
prepatch (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. Js
export 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.
  constneedsForceUpdate = !! ( 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.

Summary: Keep-alive caches components when they are first rendered. When the cache renders, keep-Alive updates the slot contents, after which $forceUpdate re-renders. This gets the latest component at render and returns the VNode from the cache if hit.

conclusion

The keep-alive component is an abstract component, which will skip the abstract component when corresponding to the parent-child relationship. It only deals with the wrapped child component, mainly caching the component VNode according to the LRU policy, and finally returning the VNode of the child component in render. The cache rendering process updates the Keep-alive slot, rerenders it, and reads the previous component VNode from the cache to implement the state cache.

Previous related articles:

Touch your hand to understand the Vue responsive principle

Hand touching takes you to understand the Computed principles of Vue

Touch your hand to understand the principle of Vue Watch

Vue you have to know about the asynchronous update mechanism and the nextTick principle

Vue view rendering principle analysis, from building vNodes to generating a real node tree