Vue source code analysis, how does keep-alive implement caching?

Preface

In terms of performance optimization, the most common method is caching. Cache resources that need to be accessed frequently to reduce the request or initialization process, thereby reducing time or memory consumption. Vue  provides us with a cache component keep-alive, which can be used for routing-level or component-level caching.

But do you know the principle of caching? How does component cache rendering work? Then 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 the component to be cached conditionally. Since there are restrictions, the old components need to delete the cache, and the new components need to be added to the latest cache. How to formulate the corresponding strategy?

LRU (Least recently used, least recently used) policies based on the data to eliminate the historical access records data . The design principle of the LRU strategy is that if a piece of data has not been accessed in the recent period, it is unlikely to be accessed in the future. That is, when limited space becomes full of data when it should be the longest not have access to the data is eliminated.

  1. Now the cache can only store up to 3 components, and the three components of ABC enter the cache in turn without any problems.
  2. When the D component is accessed, the memory space is insufficient, and A is the earliest and the oldest component, so the A component is deleted from the cache, and the D component is added to the latest position
  3. When component B is accessed again, because B is still in the cache, B moves to the newest position, and other components move to the next one.
  4. When the E component is accessed, the memory space is insufficient, C becomes the least used component, the C component is deleted from the cache, and the E component is added to the latest position

The keep-alive caching mechanism is to set the freshness of cache components according to the LRU strategy, and delete components that have not been accessed for a long time from the cache. After understanding the caching mechanism, then enter the source code to see how the keep-alive component is implemented.

 

Component realization 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)
    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
      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]) {
        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])
  }
}

Kepp-alive is actually an abstract component that only processes the child components of the package, and does not establish a parent-child relationship with the child components, nor is it rendered as a node on the page. Set abstract to true at the beginning of the component, which means that the component is an abstract component.

// 源码位置: 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
  // ...
}

So how do abstract components ignore this relationship? In the initialization phase, initLifecycle is called to determine whether the parent is an abstract component. If it is an abstract component, the upper level of the abstract component is selected as the parent, ignoring the hierarchical relationship with abstract components and child components.

Going back to the keep-alive component, the component does not write a
template, but the render  function determines the rendering result.

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

If keep-alive has multiple child elements, keep-alive requires only one child element to be rendered at the same time. So at the beginning, the child elements in the slot are obtained, and getFirstComponentChild is called to obtain the VNode of the first child element.

// 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
}

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
}

Then determine whether the current component meets the caching conditions. If the component name does not match include or exclude, it will exit and return to VNode without using the caching mechanism.

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]) {
  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

The matching condition will enter the logic of the cache mechanism. If it hits the cache, get the cached instance from the cache and set it to the current component, and adjust the position of the key to put it at the end. If it misses the cache, cache the current VNode and add the key of the current component. If the number of cache components exceeds the value of max, that is, the cache space is insufficient, call pruneCacheEntry to delete the oldest components from the cache, that is, the components of keys[0]. Then mark the keepAlive of the component as true, indicating that it is a cached component.

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)
}

pruneCacheEntry is responsible for deleting the component from the cache. It will call the component $destroy method to destroy the component instance, empty the cache component, and remove 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) {
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

Keep-alive will monitor the changes of include and exclude in mounted, adjust the order of cache and keys when the attribute changes, and finally call pruneCacheEntry.

Summary : Cache is used to cache components, and keys store the keys of components. The cache components are adjusted according to the LRU strategy. The VNode of the component is returned in the render of keep-alive, so we can also draw a conclusion that keep-alive is not really not rendered, but the rendered object is a sub-component of the package.

 

Component rendering process

Warm reminder: This part of the content requires an understanding of the render and patch process

The two main processes of the rendering process are render and patch. Before render, there will be template compilation. The render  function is the product of template compilation. It is responsible for building the VNode tree. The built VNode will be passed to the patch. The patch is based on the VNode's The relationship generates the real dom node tree.

This picture describes the  process of  vue view rendering:

After VNode is built, it will eventually be converted into real dom, and patch is a necessary process. In order to better understand the process of component rendering, it is assumed that keep-alive includes two components, A and B, and A component is displayed by default.

 

Initial rendering

The component will execute createComponent to mount the component during the patch process, and component A is no exception.

// 源码位置: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
    }
  }
}

is react ivated Identifies whether the component is reactivated. When initializing rendering, component A has not been initialized and constructed, and componentInstance is still undefined. The keepAlive of component A is true, because keep-alive, as the parent package component, will be mounted before component A, that is, kepp-alive will execute the render process first, component A will be cached, and then the first component in the slot will be cached. The keepAlive value of a component (component A) is set to true. If you don't remember this process, please see the code implemented by the above component . So at this time is react ivated is false.

Then the init function will be called  to initialize the component, 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)
    }
  },
  // ...
}

In createComponentInstanceForVnode, new  vue will  construct a component instance and assign it to componentInstance, and then call $mount to mount the component.

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
}

Call initComponent to assign vnode.elm to the real dom, and then call insert to insert the real dom of the component into the parent element.

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

 

Cached rendering

When switching to component B and then switching back to component A, component A hits the cache and is reactivated.

Going through the patch process again, keep-alive obtains the current components based on the slot, so how does the content of the slot update to realize the cache?

const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
  // patch existing root node
  patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}

During non-initial rendering, patch will call patchVnode to compare the old and new nodes.

// 源码位置: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)
  }
  // ...
}

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
  )
},

updateChildComponent is the key method for updating, which mainly updates some properties of the instance:

// 源码位置: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.
  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()
  }
}

From the comments, you can see that needsForceUpdate is true only if there is a slot, and keep-alive meets the conditions. First call resolveSlots to update the keep-alive slot, then call $forceUpdate to make the keep-alive re-render, and then go through the render again. Because the A component has been cached during initialization, keep-alive directly returns the cached A component VNode. After 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
    }
  }
}

The A component goes through the createComponent process again and calls 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)
    }
  },
}

At this time, the logic of $mount will no longer be followed, and only prepatch will be called to update the instance properties. So when the cache component is activated, the created and mounted life cycle functions will not be executed .

Back to createComponent, at this time isReactivated is true, 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)
}

Finally, insert is called to insert the dom node of the component, and the cache rendering process is complete.

Summary : When the component is rendered for the first time, keep-alive will cache the component. When the cache is rendered, keep-alive will update the slot content, and then $forceUpdate will re-render. In this way, the latest component is obtained at render time, and the VNode is returned from the cache if it hits the cache.

 

to sum up

The keep-alive component is an abstract component. It skips the abstract component when corresponding to the parent-child relationship. It only processes the packaged child components. It mainly caches the component VNode according to the LRU strategy, and finally returns the VNode of the child component during render. The cache rendering process will update the keep-alive slot, re-render it again, and read the previous component VNode from the cache to implement state caching.

Guess you like

Origin blog.csdn.net/weixin_43844696/article/details/108405111