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.