Keep-alive running process

Keep-alive is an abstract component (or functional component) that is not actually rendered in the DOM tree. It caches components in memory (without destroying them), keeps all of their states until the next rendering, and triggers the Activated hook function. This article uses dynamic components as an example: the keep-alive template

<div id="app"><keep-alive><component :is="view"></component></keep-alive><button @click="changeView"Switch > < / button > < / div >Copy the code

The child component templates are respectively

Vue.component('view1', {
    template: '<div>view component1</div>'
})
Vue.component('view2', {
    template: '<div>view component2</div>'
})
Copy the code

.

Parsing of keep-alive parent-child components

The dynamic component component tag element is handled by processComponent during execution of the closeElement function

function processComponent (el) {
    var binding;
    if ((binding = getBindingAttr(el, 'is'))) {
      el.component = binding;
    }
    if (getAndRemoveAttr(el, 'inline-template') != null) {
      el.inlineTemplate = true; }}Copy the code

generate

{
  component: 'view'
}
Copy the code

That is, the AST node is

{
  attrsList: [],
  attrsMap: {:is: "view"},
  children: [],
  component: "view",
  end: 60,
  parent: {type: 1, tag: "keep-alive", attrsList: Array(0), attrsMap: {... }, rawAttrsMap: {... },... } plain:false
  rawAttrsMap: {:is: {
    end: 47,
    name: ":is",
    start: 37,
    value: "view"
  }},
  start: 26,
  tag: "component".type: 1}Copy the code

The genElement function is called during the regeneration code phase

var code;
if (el.component) {
  code = genComponent(el.component, el, state);
} else {
  var data;
  if(! el.plain || (el.pre && state.maybeComponent(el))) { data = genData$2(el, state);
  }

  var children = el.inlineTemplate ? null : genChildren(el, state, true);
  code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : ' ') + (children ? ("," + children) : ' ') + ")";
}
Copy the code

Specific for genComponent

function genComponent (
    componentName,
    el,
    state
  ) {
    var children = el.inlineTemplate ? null : genChildren(el, state, true);
    return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : ' ') + ")")}Copy the code

return

"_c(view,{tag:"component"})"
Copy the code

Then add the keep-alive component generation

"_c('keep-alive',[_c(view,{tag:"component"})],1)"
Copy the code

The full form of the parent component’s render is

_c('div',{
    attrs:{"id":"app"}
}, [
    _c('keep-alive',
    [
        _c(view, {tag: "component"})], 1),
        _v(""),
        _c('button',{on:{"click":changeView}},[_v("Switch")])], 1)Copy the code

Where _c indicates createElem creates the element vnode, and _v creates the text vnode. The keep-alive sub-component becomes _c(view, {tag: “Component “})], 1) then generates the vNode as follows:

function createElement (
    context,
    tag,
    data,
    children,
    normalizationType,
    alwaysNormalize
  ) {
    ...
    return _createElement(context, tag, data, children, normalizationType)
  }

Copy the code

Data = {tag: “component”}, tag = view, _createElement =

. var vnode, ns;if (typeof tag === 'string') {
      var Ctor;
      ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
      if (config.isReservedTag(tag)) {
        // platform built-in elements
        vnode = new VNode(
          config.parsePlatformTagName(tag), data, children,
          undefined, undefined, context
        );
      } else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options.'components'Vnode = createComponent(Ctor, data, context, children, tag))) {// Component // Create child vNode vnode = createComponent(Ctor, data, Context, children, tag); }else{ // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ); }}else {
      // direct component options / constructor
      vnode = createComponent(tag, data, context, children);
    }
Copy the code

View1 is not an element node, so execute vnode = createComponent(Ctor, data, Context, children, tag)

function createComponent (
    Ctor,
    data,
    context,
    children,
    tag
  ) {
    ...
    data = data || {};

    // resolve constructor options in case global mixins are applied after
    // component constructor creation
    resolveConstructorOptions(Ctor);

    // extract props
    var propsData = extractPropsFromVNodeData(data, Ctor, tag);

    // functional component
    if (isTrue(Ctor.options.functional)) {
      return createFunctionalComponent(Ctor, propsData, data, context, children)
    }

    // extract listeners, since these needs to be treated as
    // child component listeners instead of DOM listeners
    var listeners = data.on;
    // replace with listeners with .native modifier
    // so it gets processed during parent component patch.
    data.on = data.nativeOn;

    if (isTrue(Ctor.options.abstract)) {
      // abstract components do not keep anything
      // other than props & listeners & slot

      // work around flow
      var slot = data.slot;
      data = {};
      if(slot) { data.slot = slot; } // Install Component management hooks onto the placeholder node // Add insert, prepatch, init, destroy to data installComponentHooks(data); //returnA placeholder vnode / / create and return vnode var name = Ctor. Options. The name | | tag; var vnode = new VNode( ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : ' ')),
      data, undefined, undefined, undefined, context,
      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
      asyncFactory
    );

    return vnode
  }
Copy the code

ComponentOptions are {Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: listeners: Children} _c(view,{tag:” Component “}

{ asyncFactory: undefined, asyncMeta: undefined, children: undefined, componentInstance: undefined, componentOptions: ƒ, propsData, listeners, tag"view1", 
    children: undefined
  },
  context: Vue {_uid: 0, _isVue: true.$options: {... }, _renderProxy: Proxy, _self: Vue,... }, data: {tag:"component", on: undefined, hook: {... }}, elm: undefined, fnContext: undefined, fnOptions: undefined, fnScopeId: undefined, isAsyncPlaceholder:false,
  isCloned: false,
  isComment: false,
  isOnce: false,
  isRootInsert: true,
  isStatic: false,
  key: undefined,
  ns: undefined,
  parent: undefined,
  raw: false,
  tag: "vue-component-1-view1",
  text: undefined,
  child: undefined
}
Copy the code

Keep alive – components

The keep-alive component is a component defined internally by Vue. Its implementation is also an object. Note that it has a property that abstract is true and is an abstract component. During initialization of initLifecycle

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

Creating a parent-child relationship between components skips the abstract component. In this example, keep-alive generates vNodes as follows:

{ asyncFactory: undefined, asyncMeta: undefined, children: undefined, componentInstance: undefined, componentOptions: ƒ, propsData: {}, listeners:"keep-alive", 
    children: Array(1)
  },
  context: Vue {_uid: 0, _isVue: true.$options: {... }, _renderProxy: Proxy, _self: Vue,... }, data: {hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ}, elm: undefined, fnContext: undefined, fnOptions: undefined, fnScopeId: undefined, isAsyncPlaceholder:false,
  isCloned: false,
  isComment: false,
  isOnce: false,
  isRootInsert: true,
  isStatic: false,
  key: undefined,
  ns: undefined,
  parent: undefined,
  raw: false,
  tag: "vue-component-3-keep-alive",
  text: undefined,
  child: undefined
}
Copy the code

Finally, the updateComponent operation is performed. Dom diff operation is performed during the mount stage, and patch (oldVnode, vNode, Hydrating, removeOnly) is executed.

. patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly); }...Copy the code

Create a node recursively and mount it to parentElem. For child components, $createElement is called, or if it is a normal element node, it is returned as follows:

functioncreateComponent (vnode, insertedVnodeQueue, parentElm, RefElm) {// I is a hook object for insert, init, prepatch, destroy var I = vnode.data;if (isDef(i)) {
    var 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

I is the hook object for insert, init, prepatch, destroy

// inline hooks to be invoked on component VNodes during patch
  var componentVNodeHooks = {
    init: function init (vnode, hydrating) {
      if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch 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);
      }
    },

    prepatch: function prepatch (oldVnode, vnode) {
      var options = vnode.componentOptions;
      var child = vnode.componentInstance = oldVnode.componentInstance;
      updateChildComponent(
        child,
        options.propsData, // updated props
        options.listeners, // updated listeners
        vnode, // new parent vnode
        options.children // new children
      );
    },

    insert: function insert (vnode) {
      var context = vnode.context;
      var componentInstance = vnode.componentInstance;
      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 */); } } }, destroy: function destroy (vnode) { var componentInstance = vnode.componentInstance; if (! componentInstance._isDestroyed) { if (! vnode.data.keepAlive) { componentInstance.$destroy(); } else { deactivateChildComponent(componentInstance, true /* direct */); }}}};Copy the code

If the component is being created for the first time, the init hook is called

createComponentInstanceForVnode(
          vnode,
          activeInstance
        );
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
Copy the code

Update component prepatch if it already exists;

Create the element instance vnode

function createComponentInstanceForVnode (
    vnode, // we know it's MountedComponentVNode but flow doesn't
    parent // activeInstance in lifecycle state
  ) {
    var options = {
      _isComponent: true,
      _parentVnode: vnode,
      parent: parent
    };
    // check inline-template render functions
    var inlineTemplate = vnode.data.inlineTemplate;
    if (isDef(inlineTemplate)) {
      options.render = inlineTemplate.render;
      options.staticRenderFns = inlineTemplate.staticRenderFns;
    }
    return new vnode.componentOptions.Ctor(options)
  }
Copy the code

_parentNode represents the current VNode, followed by the initialization of the child components

var Sub = function VueComponent (options) {
        this._init(options);
      };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.cid = cid++;
    Sub.options = mergeOptions(
    Super.options,
    extendOptions
    );
    Sub['super'] = Super
Copy the code

$slots={default: [vNode]} is initialized in the initRender initialization function. Since we write the DOM inside the tag, we can get its default slot first and then its first child. We only deal with the first child element, so we usually use it with component dynamic components or router-view vNodes, $scopedSlots, vm.$createElement to get the child component to render. The child _render execution process handles this

vm.$scopedSlots = normalizeScopedSlots {
    ...
    return{default: ƒ ()$hasNormal: true
        $key: undefined
        $stable: false
    }
}
vnode = render.call(vm._renderProxy, vm.$createElement);
Copy the code

The render function now points to vm._renderProxy, the keep-alive built-in component, or executes if the current child component is keep-alive

var KeepAlive = {
    name: 'keep-alive',
    abstract: true,
    props: {
      include: patternTypes,
      exclude: patternTypes,
      max: [String, Number]
    },
    created: function created () {
      this.cache = Object.create(null);
      this.keys = [];
    },

    destroyed: function destroyed () {
      for (var key in this.cache) {
        pruneCacheEntry(this.cache, key, this.keys);
      }
    },

    mounted: function mounted () {
      var thisThe $1 = this;

      this.$watch('include'.function (val) {
        pruneCache(thisThe $1.function (name) { return matches(val, name); });
      });
      this.$watch('exclude'.function (val) {
        pruneCache(thisThe $1.function (name) { return! matches(val, name); }); }); }, render:function render() {// Get the subcomponent content [VNode] var slot = this.$slots.default;
      var vnode = getFirstComponentChild(slot);
      var componentOptions = vnode && vnode.componentOptions;
      if(componentOptions) {var name = getComponentName(componentOptions); var ref = this; var include = ref.include; var exclude = ref.exclude; // If include is included and does not match, or exclude is included and does not match, the vNode of the component is returned directlyif( // 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
          // 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; // Check if there is any in the cache"1::view1"If yes, fetch the component instance directly from the cache and update the key in keysif (cache[key]) {
          vnode.componentInstance = cache[key].componentInstance;
          // make current key freshest
          remove(keys, key);
          keys.push(key);
        } else{// cache vnode cache[key] = vnode; keys.push(key); // Prune simple entry // If Max is configured and the cache size exceeds this. Max, remove the first entry from the cacheif (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

The pruneCacheEntry function is:

functionpruneCacheEntry ( 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

If the cached component tag is not the same as the currently rendered component tag, the $destroy method is also executed to destroy the cached component instance and finally set vnode.data.keepAlive = true. In addition, keep-alive also uses watch to detect changes in the incoming include and exclude, and to process the cache, that is, to iterate the cache. If the node name in the cache does not match the new rule, the cache node is removed from the cache. The vNode generated by the child component of keep-Alive is:

{ asyncFactory: undefined asyncMeta: undefined, children: undefined, componentInstance: undefined, componentOptions: ƒ, propsData, listeners, tag"view1", 
    children: undefined
  },
  context: Vue {_uid: 0, _isVue: true.$options: {... }, _renderProxy: Proxy, _self: Vue,... }, data: { tag:"component", on: undefined, hook: {... }, keepAlive:true
  },
  elm: undefined,
  fnContext: undefined,
  fnOptions: undefined,
  fnScopeId: undefined,
  isAsyncPlaceholder: false,
  isCloned: false,
  isComment: false,
  isOnce: false,
  isRootInsert: true,
  isStatic: false,
  key: undefined,
  ns: undefined,
  parent: undefined,
  raw: false,
  tag: "vue-component-1-view1",
  text: undefined,
  child: undefined,
}
Copy the code

CreateElm then calls createComponent in the render phase, which initializes the View1 component and compiles the template to generate the Render function

(function anonymous(
) {
with(this){return _c('div',[_v("view component1")]}})Copy the code

Finally, the rendering process.

For the first time to render

In the final render phase, createElm executes createComponent, which triggers init hooks in componentVNodeHooks. Vnode.data. keepAlive is set to true, so else will be entered, following the normal mount process:

// The keep-alive property is set totrue, so the vnode. Data. KeepAlive =true
init: function init (vnode, hydrating) {
  if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch var mountedNode = vnode; // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode); }else{/ / the view1 dynamic components mounted to the parent (not keep alive - components) child = vnode.com ponentInstance = createComponentInstanceForVnode var (vnode, activeInstance ); // The child node is mounted to the parent component child.$mount(hydrating ? vnode.elm : undefined, hydrating); }}Copy the code

Mount it step by step and render it to the page.

Cache rendering process

When a component is switched to another component, the old and new vNodes and their child nodes are compared during the patch process, while the keep-alive component is updated. First, during the patchVnode process, an element is about to be repaired and the prepatch hook function is executed:

if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
  i(oldVnode, vnode);
}
Copy the code

namely

prepatch: function prepatch (oldVnode, vnode) {
      var options = vnode.componentOptions;
      var 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

UpdateChildComponent (Child, options.propsData, // Updated props Options. listeners, // updated listeners vnode, // new parent vnode options.children // new children );

The core code of this function:Copy the code

// renderChildren is the latest child of [VNode], vm.$options._renderChildren is the old child of [VNode].

var 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(a); }Copy the code

ResolveSlots assigns the VNode of the new child component to VM.$slots, that is

vm.$slots = {
    default: [VNode]
}
Copy the code

Force update and re-render

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

The createComponent function is executed again

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  var i = vnode.data;
  if(isDef(I)) {// When updated, isReactivated istrue
    var 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

Where data is:

{hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ} keepAlive:true,
    on: undefined,
    tag: "component",}Copy the code

Above I (vnode, false /* hydrating */), execute

init: function init (vnode, hydrating) {
  if( vnode.componentInstance && ! Vnode.ponentinstance._isdestroyed &&vnode.data.keepalive) {// Update process patch // kept-alive Components, treat as a patch var mountedNode = vnode; // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode); }}Copy the code

The init hook does not mount the component. Instead, it returns to the createComponent function, which executes the reactivateComponent method if isReactivated is true

functionreactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) { var i; var innerNode = vnode; . // unlike a newly created component, // a reactivated keep-alive component doesn't insert itself insert(parentElm, vnode.elm, refElm); }Copy the code

Inserts the cached DOM object directly into the target element.