[Vue source code] logic analysis of keep-alive components (Part 2)

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

Foreword

It is recommended to read two chapters before reading this chapter

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

[Vue source code] logic analysis of keep-alive components (in)

This chapter mainly analyzes the life cycle of the keep-alive component, including the creation, destruction, mounting, etc. of the component; in addition, the keep-alive used A brief analysis of the obtained cache algorithm, so as to have a certain understanding of the optimization strategy of keep-alive in memory usage

Lifecycle

When subcomponents wrapped by keep-alive are rendered again, the mounted lifecycle hook will not be executed, only the activated hook will be executed. And when the subcomponent is hidden, the destroyed life cycle hook will not be executed, but the deactivated hook will be executed

Assuming that in the dynamic component wrapped by keep-alive, it is possible to switch between child1 and child2, then when starting from child1 switches to child2, the child1 component will execute the deactivated hook, and when it is switched from child2 again When switching back to child1, deactivated of child2 will be executed, and then activated of child1 will be executed > hook

deactivated

Let’s start with component destruction, when switching from child1 to child2, child1 will execute the deactivated hook instead of destroyed hook. In the previous analysis of patch, the changes of the old and new nodes will be compared, so as to operate the real DOM as small as possible. When diff is completed After operating the node, the next important step is to destroy and remove the old components.

function patch (oldVnode, vnode, hydrating, removeOnly) {<!-- -->
  // destroy old node
  if (isDef(parentElm)) {<!-- -->
    removeVnodes(parentElm, [oldVnode], 0, 0)
  } else if (isDef(oldVnode. tag)) {<!-- -->
    invokeDestroyHook(oldVnode)
  }
}

function removeVnodes (parentElm, vnodes, startIdx, endIdx) {<!-- -->
  for (; startIdx <= endIdx; + + startIdx) {<!-- -->
    // Get the component that needs to be removed
    const ch = vnodes[startIdx]
    if (isDef(ch)) {<!-- -->
      if (isDef(ch. tag)) {<!-- -->
        // remove operation of real node
        removeAndInvokeRemoveHook(ch)
        invokeDestroyHook(ch)
      } else {<!-- --> // Text node
        removeNode(ch.elm)
      }
    }
  }
}

removeAndInvokeRemoveHook will remove the old node, and the key step is to delete the real node from the parent element. invokeDestroyHook is the core of destroying component hooks. If there are subcomponents under this component, invokeDestroyHook will be called recursively to perform the destruction operation. The destruction process will execute the destroy hook defined inside the component

function invokeDestroyHook (vnode) {<!-- -->
  let i, j
  const data = vnode.data
  if (isDef(data)) {<!-- -->
    if (isDef(i = data.hook) & amp; & amp; isDef(i = i.destroy)) i(vnode)
    // Execute the destroy hook function inside the component
    for (i = 0; i < cbs.destroy.length; + + i) cbs.destroy[i](vnode)
  }
  // If the component has subcomponents, traverse the subcomponents and call invokeDestroyHook recursively to execute the hook
  if (isDef(i = vnode.children)) {<!-- -->
    for (j = 0; j < vnode.children.length; + + j) {<!-- -->
      invokeDestroyHook(vnode. children[j])
    }
  }
}

The two internal hooks of the component init prePatch have been analyzed before, let’s take a look at the destroy hook function

const componentVNodeHooks = {<!-- -->
  destroy (vnode: MountedComponentVNode) {<!-- -->

    // Get the component instance
    const {<!-- --> componentInstance } = vnode
    if (!componentInstance._isDestroyed) {<!-- -->
      if (!vnode.data.keepAlive) {<!-- -->
        // If it is not a keep-alive component, execute the destroy operation
        componentInstance. $destroy()
      } else {<!-- -->
        // If it is a cached component
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

When the component is a component cached by keep-alive, that is, a component marked with keepAlive, the instance will not be destroyed, that is, componentInstance.$ destroy() procedure. The $destroy() process will do a series of component destruction operations, and the beforeDestroy and destroyed hook functions are also in $destroy code> is called. The process of deactivateChildComponent is completely different from $destroy.

export function deactivateChildComponent (vm: Component, direct?: boolean) {<!-- -->
  if (direct) {<!-- -->
    vm._directInactive = true
    if (isInInactiveTree(vm)) {<!-- -->
      return
    }
  }
  if (!vm._inactive) {<!-- -->
    // mark the component as disabled
    vm._inactive = true
    for (let i = 0; i < vm.$children.length; i ++ ) {<!-- -->
      // Called recursively when subcomponents exist.
      deactivateChildComponent(vm. $children[i])
    }
    // call the deactivated hook
    callHook(vm, 'deactivated')
  }
}

_directInactive is used to mark whether the disabled component is the topmost component. And _inactive is a deactivated mark, and the child component also needs to recursively call deactivateChildComponent to mark it as deactivated. Finally, the user-defined deactivated hook function will be executed

activated

Also in the patch process, when the old component is removed and destroyed or deactivated, the corresponding hook will be executed for the new component. This is why disabled hooks are executed before enabled hooks.

function patch (oldVnode, vnode, hydrating, removeOnly) {<!-- -->
  {<!-- -->
    // destroy or deactivate the node
    if (isDef(parentElm)) {<!-- -->
      removeVnodes(parentElm, [oldVnode], 0, 0)
    } else if (isDef(oldVnode. tag)) {<!-- -->
      invokeDestroyHook(oldVnode)
    }
  }

  // insert node
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

function invokeInsertHook (vnode, queue, initial) {<!-- -->
  // delay insert hooks for component root nodes, invoke them after the
  // element is really inserted
  // When the node has been inserted, the execution of the insert hook will be delayed
  if (isTrue(initial) & amp; & amp; isDef(vnode. parent)) {<!-- -->
    vnode.parent.data.pendingInsert = queue
  } else {<!-- -->
    for (let i = 0; i < queue. length; + + i) {<!-- -->
      // Call the insert hook function inside the component
      queue[i].data.hook.insert(queue[i])
    }
  }
}

Let’s look at the specific implementation of the component’s insert hook function

const componentVNodeHooks = {<!-- -->
  insert (vnode: MountedComponentVNode) {<!-- -->
    const {<!-- --> context, componentInstance } = vnode
    if (!componentInstance._isMounted) {<!-- -->
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {<!-- -->
      if (context._isMounted) {<!-- -->
        //vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {<!-- -->
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },
}

When the component is instantiated for the first time, because the _isMounted property of the component does not exist, the mounted hook function will be called, when the child2 is cut again When returning to child1, since child1 is only deactivated but not destroyed, the mounted hook function will not be called again, and the mounted hook function will be executed at this time. The code>activateChildComponent function processes the state of the component. activateChildComponent is similar to the previously analyzed deactivateChildComponent method, which updates the activation state of the component.

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) {<!-- -->
    // mark component is enabled
    vm._inactive = false
    for (let i = 0; i < vm.$children.length; i ++ ) {<!-- -->
      // Recursively process the enabled state of child components.
      activateChildComponent(vm. $children[i])
    }
    callHook(vm, 'activated')
  }
}

Cache optimization – LRU algorithm

The memory space of the program is limited, so we cannot store data without restraint. This requires a strategy to eliminate less important data and keep the maximum data storage consistent.

According to different elimination mechanisms, the following three types are commonly used:

    1. FIFO: first-in-first-out policy By recording the usage of data usage, when the buffer size is about to overflow, the data with the farthest current time in the cache will be cleared first
    1. LRU: Least Recently Used When the LRU policy follows the principle, if the data has been used (accessed) recently, then the probability of being accessed in the future is considered to change. If an array is used to record data, when a piece of data is accessed, the data will be moved to the end of the array, indicating that the data has been used recently. When the buffer overflows, the data at the head of the data will be deleted, that is, the least used Data removal.

    For the LRU algorithm, I personally think that it would be more appropriate to translate it into the most recently unused. In the implementation of the algorithm, it does not pay attention to the number of times the data is used in the specified time period, but directly eliminates the last usage time The oldest data from the current time

    1. LFU: Least count strategy records the number of times each data is used, and when the buffer overflows, the data with the least number of times is eliminated.

The above three cache algorithms have their own advantages and disadvantages, and they are used in different scenarios. As for the optimization of keep-alive in Vue when caching components, it is obvious that the cache strategy of LRU is used. Let’s take a look at the key code

export default {<!-- -->
  // render function
  render () {<!-- -->
    if (cache[key]) {<!-- -->
      // Hit the cache,
      // make current key freshest
      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)
      }
    }
  }
}

export function remove (arr: Array<any>, item: any): Array<any> | void {<!-- -->
  if (arr. length) {<!-- -->
    const index = arr. indexOf(item)
    if (index > -1) {<!-- -->
      return arr. splice(index, 1)
    }
  }
}

Every time the render function is executed, it will first check whether there is a cache for the corresponding component in the cache. If there is no cache, add the cache and save the key of the component in keys At the end of the data, it indicates that this component is the subcomponent of keep-alive rendered last time. If the cache is found, remove the component’s key from the keys array, add it back to the end of the data, and perform the same operation each time.

When the size of the cached array exceeds the specified size, delete the data in the head of the keys array and delete the cached data corresponding to key, because keys The component corresponding to the key in the header is the component data that has not been used for a long time, which conforms to the LRU algorithm strategy.