In the previous section thoroughly understanding the magic of Keep-Alive in Vue (part 1), we did source analysis of the initial rendering flow of the Keep-Alive component and the configuration information of the component. The most critical step in the initial rendering process is the caching of the rendered component Vnode, which also includes the actual node storage of the component. With the first cache, what magic does Keep-Alive have when the component is rendered again? Now we’re going to unveil it once and for all.

13.5 Preparations

The previous section on the analysis of the Keep-Alive component started with a flow chart I drew. If you don’t want to go back to the last section, here’s a quick summary.

    1. keep-aliveIs a component option configuration defined internally in the source code, which is first registered as a global component for developers to use globally, whererenderFunction defines its rendering process
    1. As with normal components, the parent is encountered during the creation of a real nodekeep-aliveThe component initializes and instantiates the component.
    1. The instantiation performs the mount$mountThis step will be executedkeep-aliveIn the optionsrenderFunction.
    1. renderThe initial render function will render the childVnodeCache. At the same timeThe corresponding child real nodes are also cached.

So how is keep-alive handled differently when it needs to render to already rendered components again?

13.5.1 Basic Usage

For the integrity of the article, I still show the basic use, including the use of life cycle, which is convenient for the subsequent analysis of keep-Alive life cycle.

<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++
        }
    },
    mounted() {
        console.log('child1 mounted')},activated() {
        console.log('child1 activated')},deactivated() {
        console.log('child1 deactivated')},destoryed() {
        console.log('child1 destoryed')
    }
}
var child2 = {
    template: '<div>child2</div>'.mounted() {
        console.log('child2 mounted')},activated() {
        console.log('child2 activated')},deactivated() {
        console.log('child2 deactivated')},destoryed() {
        console.log('child2 destoryed')
    }
}

var vm = new Vue({
    el: '#app',
    components: {
        child1,
        child2,
    },
    data() {
        return {
            chooseTabs: 'child1', } }, methods: { changeTabs(tab) { this.chooseTabs = tab; }}})Copy the code
13.5.2 flowchart

Consistent with the analysis of the first rendering, I drew a simple flow chart for the second rendering.

13.6 Process Analysis

13.6.1 Rerendering components

The re-rendering process starts with data changes, in this case, changes to chooseTabs data in a dynamic component that causes dependency distribution of updates. (Three articles in this series detail the underlying implementation of a VUE responsive system for those interested.) Simply put, chooseTabs is a data that collects dependencies that use the data during initialization. When data changes, collected dependencies are dispatched for updates.

The process responsible for instance mounting in the parent component is executed as a dependency, i.e. Vm._update (vm._render(), hydrating) of the parent component; . _render and _update represent two processes respectively, where the _render function generates a new Vnode for the component based on changes in data, and the _update function eventually generates a real node for the new Vnode. And in the process of generating real nodes, will use Vitrual DOM diff algorithm before and after vnode nodes are compared, so that as little as possible to change the real node, this part of the content can be reviewed in-depth analysis of Vue source code – to, with me to achieve the DIFF algorithm! , which elaborates the idea of using diff algorithm to compare node differences.

Patch is the process of comparing old and new VNodes, while patchVnode is the core step. We ignore other processes of patchVnode and pay attention to the prepatch hook execution of sub-components.

functionPatchVnode (oldVnode vnode, insertedVnodeQueue ownerArray, index, removeOnly) {... / / new vnode execution prepatch hookif(isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode); }...}Copy the code

Executing the PrePatch hook takes the new and old component instances and executes the updateChildComponent function. The updateChildComponent updates the status of the old instance for the new component instance, including props,listeners, etc., and ultimately re-renders the instance by calling the global vm.$forceUpdate() method provided by vue.

Var componentVNodeHooks = {//function() {},
    prepatch: functionPrepatch (oldVnode, vnode) {var options = vnode.componentOptions; / / the old component instance var child ponentInstance = = vnode.com oldVnode.com ponentInstance; updateChildComponent( child, options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children ); }},function updateChildComponent() {// Update the old state without analyzing the process... // force the instance to re-render. vm.$forceUpdate(a); }Copy the code

Let’s take a look at what $forceUpdate does. $forceUpdate is an API exposed to the source code. They force Vue instances to re-render, essentially executing the dependencies collected by the instance. In this case, Watcher corresponds to the keep-alive vm._update(vm._render(), hydrating); Process.

Vue.prototype.$forceUpdate = function () {
    var vm = this;
    if(vm._watcher) { vm._watcher.update(); }};Copy the code
13.6.2 Reusing cache components

Since vm.$forceUpdate() forces the keep-alive component to re-render, the keep-alive component performs the render process again. This time because of the first vNode cache, keep-Alive found the cached component in the cache object of the instance.

// Keepalive component option var keepalive = {name:'keep-alive',
    abstract: true,
    render: function render() {// get the value of the keep-alive slot var slot = this.$slots.default; Var vNode = getFirstComponentChild(slot); Var componentOptions = vnode&&vnode.componentOptions; // The first child of keep-alive existsifVar name = getComponentName(componentOptions); var ref = this; var include = ref.include; var exclude = ref.exclude; // Determine whether the child meets the cache matchif( // not included (include && (! name || ! matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) {return vnode
        }

        var refThe $1 = this;
        var cache = refThe $1.cache;
        var keys = refThe $1.keys;
        var key = vnode.key == null ? componentOptions.Ctor.cid + (componentOptions.tag ? ("... "" + (componentOptions.tag)) : ' ') : vnode.key; // ==== focus here ====if(cache[key]) {vnode.componentInstance = cache[key]. ComponentInstance; // remove(keys, key); keys.push(key); }else{// First render vnode cache[key] = vnode; keys.push(key); // prune oldest entryif (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

Render (); render (); render (); render (); render (); render (); render (); If you’re reading this, you might be confused about what keys does in the source code, and what pruneCacheEntry does, but I’ll leave that at the end of the article when I talk about cache optimization strategies.

13.6.3 Replacing real Nodes

The _render process for the keep-alive component is performed, followed by the _update to generate the actual node. Again, there is a child1 child under keep-alive, So the _update procedure will call the createComponent recursively to create the child vNode. This process was also analyzed during the first render, so we can compare how the process is different when rendering again.

functionCreateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {// Vnode is the cache vnode var I = vnode.data;if(isDef(I)) {// isReactivated istrue
        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
        if (isDef(i = i.hook) && isDef(i = i.init)) {
          i(vnode, false /* hydrating */);
        }
        if(isDef(vnode.componentInstance)) {// One of the functions is to retain the real DOM in the vnode initComponent(vnode, insertedVnodeQueue); insert(parentElm, vnode.elm, refElm);if (isTrue(isReactivated)) {
            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
          }
          return true}}}Copy the code

KeepAlive = true; vnode.data.keepalive = true; , so the value of isReactivated is true, and i.init still performs the initialization of the child components. But this process is not exactly the same because of caching.

var componentVNodeHooks = {
    init: function init (vnode, hydrating) {
      if( vnode.componentInstance && ! Vnode.ponentinstance. _isDestroyed &&vnode.data.keepalive) { Run prepatch hook var mountedNode = vnode; // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode); }else {
        var child = vnode.componentInstance = createComponentInstanceForVnode(
          vnode,
          activeInstance
        );
        child.$mount(hydrating ? vnode.elm : undefined, hydrating); }}},Copy the code

Obviously, because of the keepAlive flag, the child component does not go through the mount process and simply performs the Prepatch hook to update the component state. It also makes good use of the real nodes reserved before caching VNode for node replacement.

13.7 Life Cycle

We use examples to see how the keep-alive life cycle differs from a normal component.

When we switch from child1 to child2 and then back to child1, chil1 does not execute a mounted hook, and child2 does not execute a destoryed hook. The deactivated hook of Child2 executes earlier than the activated hook of child1.

13.7.1 deactivated

When child1 switches to child2, it executes the Deactivated hook instead of the DeStoryed hook. The previous patch analysis process will compare the changes of the old and new nodes, so as to operate the real nodes as small as possible. After completing the DIff algorithm and node operation, the next important step is to perform the destruction and removal operation of the old components. The code for this step is as follows:

functionPatch (···) {// Analyzed patchVnode process // Destroyed old nodeif (isDef(parentElm)) {
    removeVnodes(parentElm, [oldVnode], 0, 0);
  } else if(isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode); }}functionRemoveVnodes (parentElm, vnodes, startIdx,endIdx) {// startIdx,endIdx are 0for(; startIdx <= endIdx; Var ch = vnodes[startIdx];if (isDef(ch)) {
      if(isDef (ch. Tag)) {/ / real nodes remove operation removeAndInvokeRemoveHook (ch); invokeDestroyHook(ch); }else{ // Text node removeNode(ch.elm); }}}}Copy the code

RemoveAndInvokeRemoveHook is to remove the old node operation, one of the key step is real nodes will be deleted from the parent element, interested to see this part of logic. InvokeDestroyHook is the core of the hook that executes the destroy component. If there are child components under this component, the invokeDestroyHook is recursively called to perform the destruction operation. The destruction process executes the deStory hook inside the component.

function invokeDestroyHook (vnode) {
    var i, j;
    var data = vnode.data;
    if (isDef(data)) {
      if(isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); } // Execute the destroy hook inside the componentfor(i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); }} // If the component has child components, loop through the child components to recursively call invokeDestoryHook to execute the hookif (isDef(i = vnode.children)) {
      for(j = 0; j < vnode.children.length; ++j) { invokeDestroyHook(vnode.children[j]); }}}Copy the code

While the init and prepatch hooks were introduced earlier, the logic of the destroy hook is simpler.

var componentVNodeHooks = {
  destroy: functionDestroy (vnode) {// componentInstance = vnode.componentInstance; // If the instance has not already been destroyedif(! Componentinstance. _isDestroyed) {// Destroy the keep-alive componentif(! vnode.data.keepAlive) { componentInstance.$destroy(a); }else{// deactivateChildComponent(componentInstance,true/* direct */); }}}}Copy the code

When a component is keep-alive cached, that is, marked with keepAlive, the instance destruction is not performed, that is, componentInstance.$destroy(). The $destroy procedure does a set of component destruction operations. BeforeDestroy, deStoryed hooks are also called in the $deStory procedure, whereas deactivateChildComponent is handled completely differently.

function deactivateChildComponent (vm, direct) {
  if (direct) {
    // 
    vm._directInactive = true;
    if (isInInactiveTree(vm)) {
      return}}if(! Vm. _inactive) {// The inactive vm._inactive =true; // Deactivate the child components as wellfor (var i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i]); } // Deactivated hook callHook(vm,'deactivated'); }}Copy the code

_directInactive is used to mark whether the disabled component is the topmost component. While _inactive is a sign of stopping, the same call deactivateChildComponent child components also need to pass to go, stop using the mark in the play. The user-defined Deactivated hook is eventually executed.

13.7.2 activated

Looking back at the execution timing of activated, the patch process also executes hooks on new nodes after removing old nodes and executing destroyed or deactivated hooks. This is why disabled hooks are executed before enabled hooks.

functionPatch (···) {// patchVnode process // Destroy old node {if (isDef(parentElm)) {
      removeVnodes(parentElm, [oldVnode], 0, 0);
    } else if(isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode); } // Execute the insert hook inside the component invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); }function invokeInsertHook (vnode, queue, initial) {
  // delay insert hooks forComponent root nodes, invoke them after the // Delay the insert hook when the node has been insertedif (isTrue(initial) && isDef(vnode.parent)) {
    vnode.parent.data.pendingInsert = queue;
  } else {
    for(var i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]); }}}Copy the code

The insert hook logic inside the same component is as follows:

Var componentVNodeHooks = {insert:functioninsert (vnode) { var context = vnode.context; var componentInstance = vnode.componentInstance; // The instance is already mountedif(! 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 */); }}}},Copy the code

When we instantiate a component for the first time, the _isMounted component does not exist, so we call the mounted hook. When we cut back from child2 to child1, we do not call the mounted hook because child1 was disabled and not destroyed. The activateChildComponent function is executed to handle the state of the component. The logic of activateChildComponent is easy to understand given the basis for analyzing deactivateChildComponent. Similarly, the _inactive flag is enabled and a recursive call to activateChildComponent does state handling.

function activateChildComponent (vm, direct) {
  if (direct) {
    vm._directInactive = false;
    if (isInInactiveTree(vm)) {
      return}}else if (vm._directInactive) {
    return
  }
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false;
    for (var i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i]);
    }
    callHook(vm, 'activated'); }}Copy the code

13.8 Cache Optimization – LRU

The memory space of the program is limited, so we cannot store data indiscriminately. At this time, we need to have a strategy to weed out the less important data and keep the maximum data storage consistent. This type of strategy is called cache optimization strategy, and there are three commonly used strategies based on the elimination mechanism.

  • 1. FIFO: First in, first out (FIFO) policy. We record the data usage time.

  • 2. LRU: least used recently. LRU strategy follows the principle is, if the data is accessed recently (use), the chance to be accessed in the future will be higher, if an array to record data, when a data is accessed, the data will be moved to the end of the array, shows that been used recently, when the buffer overflow, will delete the array data, the head is the least frequently used data is removed.

  • 3. LFU: minimum count policy. Number of times to mark the frequency of data usage, the least number of times will be eliminated in the case of cache overflow.

Each of these three caching algorithms has its advantages and disadvantages, and is suitable for different scenarios. However, when we look at the optimization processing of keep-Alive during caching, it is obvious that LRU cache strategy is used. Let’s look at the key code

var keepAlive = {
  render: function() {...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); }}}}function remove (arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}
Copy the code

The implementation of cache logic is analyzed with a practical example.

    1. There are three componentschild1,child2,child3.keep-aliveSet the maximum number of caches to 2
    1. withcacheObject to store the componentvnode.keyIs the component name,valueFor a componentvnodeObject, withkeysArray to record the component name, because is an array, sokeysFor the order.
    1. child1,child2Components are accessed in sequence, and the cache result is
keys = ['child1'.'child2']
cache = {
  child1: child1Vnode,
  child2: child2Vnode
}
Copy the code
    1. Revisit tochild1Component, due to a cache hit, is calledremoveMethods thekeysIn thechild1Delete and pass the arraypushMethod will bechild1Push it to the tail. The cache result is changed to
keys = ['child2'.'child1']
cache = {
  child1: child1Vnode,
  child2: child2Vnode
}
Copy the code
    1. Access to thechild3Because the number of caches is limited, the initial cache is executedpruneCacheEntryMethod deletes the least accessed data.pruneCacheEntryIs defined as follows
function pruneCacheEntry (cache,key,keys,current) {
    var cached$The $1= cache[key]; // Destroy the instanceif (cached$The $1&& (! current || cached$The $1.tag ! == current.tag)) { cached$The $1.componentInstance.$destroy(a); } cache[key] = null; remove(keys, key); }Copy the code

Keys [0] will be removed from the cache. The most recently accessed element will be at the end of the array, so header data is usually the least accessed, so header elements will be deleted first. The remove method is called again to remove the first element of keys.

This is how vUE optimizes the keep-alive cache handling.


  • An in-depth analysis of Vue source code – option merge (1)
  • An in-depth analysis of Vue source code – option merge (2)
  • In-depth analysis of Vue source code – data agents, associated child and parent components
  • In-depth analysis of Vue source code – instance mount, compile process
  • In-depth analysis of Vue source code – complete rendering process
  • In-depth analysis of Vue source code – component foundation
  • In-depth analysis of Vue source code – components advanced
  • An in-depth analysis of Vue source code – Responsive System Building (PART 1)
  • In – Depth Analysis of Vue source code – Responsive System Building (Middle)
  • An in-depth analysis of Vue source code – Responsive System Building (Part 2)
  • In-depth analysis of Vue source code – to implement diff algorithm with me!
  • In-depth analysis of Vue source code – reveal Vue event mechanism
  • In-depth analysis of Vue source code – Vue slot, you want to know all here!
  • In-depth analysis of Vue source code – Do you understand the SYNTAX of V-Model sugar?
  • In-depth analysis of Vue source – Vue dynamic component concept, you will be confused?
  • Thoroughly understand the keep-alive magic in Vue (part 1)
  • Thoroughly understand the keep-alive magic in Vue (2)