keep-alive是個抽象組件(或稱爲功能型組件),實際上不會被渲染在DOM樹中。它的做用是在內存中緩存組件(不讓組件銷燬),等到下次再渲染的時候,還會保持其中的全部狀態,而且會觸發activated鉤子函數。通常與或者動態組件配合使用,本篇以動態組件爲例: keep-alive的模板vue
<div id="app"><keep-alive><component :is="view"></component></keep-alive><button @click="changeView">切換</button></div>
複製代碼
子組件模板分別爲node
Vue.component('view1', {
template: '<div>view component1</div>'
})
Vue.component('view2', {
template: '<div>view component2</div>'
})
複製代碼
。react
動態組件component標籤元素會在closeElement函數執行過程當中,由processComponent處理vue-router
function processComponent (el) {
var binding;
if ((binding = getBindingAttr(el, 'is'))) {
el.component = binding;
}
if (getAndRemoveAttr(el, 'inline-template') != null) {
el.inlineTemplate = true;
}
}
複製代碼
生成緩存
{
component: 'view'
}
複製代碼
即AST節點爲bash
{
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
}
複製代碼
再生成代碼階段,genElement函數會調用app
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) : '') + ")";
}
複製代碼
genComponent具體爲dom
function genComponent (
componentName,
el,
state
) {
var children = el.inlineTemplate ? null : genChildren(el, state, true);
return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")")
}
複製代碼
返回async
"_c(view,{tag:"component"})"
複製代碼
而後添加keep-alive組件生成函數
"_c('keep-alive',[_c(view,{tag:"component"})],1)"
複製代碼
最後父組件的render生成的完整形式爲
_c('div',{
attrs:{"id":"app"}
}, [
_c('keep-alive',
[
_c(view, {tag: "component"})], 1),
_v(" "),
_c('button',{on:{"click":changeView}},[_v("切換")
])
], 1)
複製代碼
其中_c表示createElem建立元素vnode, _v建立文本類型的vnode。keep-alive的子組件component變爲_c(view, {tag: "component"})], 1)
而後生成vnode的過程以下:
function createElement (
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
...
return _createElement(context, tag, data, children, normalizationType)
}
複製代碼
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', tag))) {
// component
// 建立子組件的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);
}
複製代碼
由於view1並非元素節點,故進入執行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
// 在data中增長insert、prepatch、init、destroy四個鉤子
installComponentHooks(data);
// return a placeholder vnode
// 建立並返回vnode
var name = Ctor.options.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
}
複製代碼
其中vnode中的componentOptions爲 {Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children} _c(view,{tag:"component"})
生成vnode爲
{
asyncFactory: undefined,
asyncMeta: undefined,
children: undefined,
componentInstance: undefined,
componentOptions: {
Ctor: ƒ,
propsData: undefined,
listeners: undefined,
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
}
複製代碼
keep-alive組件是Vue內部定義的組件,它的實現也是一個對象,注意它有一個屬性 abstract 爲 true,是一個抽象組件。在初始化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
複製代碼
組件之間創建父子關係會跳過該抽象組件,這個例子中的 keep-alive生成的VNode爲:
{
asyncFactory: undefined,
asyncMeta: undefined,
children: undefined,
componentInstance: undefined,
componentOptions: {
Ctor: ƒ,
propsData: {},
listeners: undefined,
tag: "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
}
複製代碼
最後進行updateComponent操做, 在掛載階段會進行dom diff操做, 執行 patch (oldVnode, vnode, hydrating, removeOnly)
...
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
}
...
複製代碼
遞歸的建立節點而後掛載到parentElem上,對於子組件,會執行$createElement函數, 若是是普通元素節點則直接返回,過程以下:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
// i 是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
}
}
}
複製代碼
上面i 是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 */); } } } }; 複製代碼
若是是初次建立組件,則調用init鉤子裏的
createComponentInstanceForVnode(
vnode,
activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
複製代碼
已存在的話更新組件調用prepatch;
建立元素實例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)
}
複製代碼
_parentNode表示當前vnode, 而後就是子組件的初始化
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
複製代碼
在initRender初始函數中會初始化$slots={default: [vnode]}
, 由於 是在標籤內部寫 DOM,因此能夠先獲取到它的默認插槽,而後再獲取到它的第一個子節點。 只處理第一個子元素,因此通常和它搭配使用的有 component 動態組件或者是 router-view獲取須要渲染的子組件的VNode、$scopedSlots、vm.$createElement
; 子組件_render執行過程會處理
vm.$scopedSlots = normalizeScopedSlots {
...
return {
default: ƒ ()
$hasNormal: true
$key: undefined
$stable: false
}
}
vnode = render.call(vm._renderProxy, vm.$createElement);
複製代碼
render函數此時指向vm._renderProxy即keep-alive內置組件, 若是當前子組件是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 this$1 = this;
this.$watch('include', function (val) {
pruneCache(this$1, function (name) { return matches(val, name); });
});
this.$watch('exclude', function (val) {
pruneCache(this$1, function (name) { return !matches(val, name); });
});
},
render: function render () {
// 獲取子組件內容 [VNode]
var slot = this.$slots.default;
var vnode = getFirstComponentChild(slot);
var componentOptions = vnode && vnode.componentOptions;
if (componentOptions) {
// check pattern
// 得到子組件的組件名
var name = getComponentName(componentOptions);
var ref = this;
var include = ref.include;
var exclude = ref.exclude;
// 若是知足了配置 include 且不匹配或者是配置了 exclude 且匹配,那麼就直接返回這個組件的 vnode
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
var ref$1 = this;
var cache = ref$1.cache;
var keys = ref$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;
// 判讀緩存裏是否有"1::view1",存在的話直接從緩存裏獲取組件實例,更新keys中的key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove(keys, key);
keys.push(key);
} else {
// 緩存vnode
cache[key] = vnode;
keys.push(key);
// prune oldest entry
// 若是配置了 max 而且緩存的長度超過了 this.max,還要從緩存中刪除第一個
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
vnode.data.keepAlive = true;
}
return vnode || (slot && slot[0])
}
};
複製代碼
其中pruneCacheEntry函數爲:
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)
}
複製代碼
若是緩存的組件標籤與當前渲染組件的tag不一致時,也執行刪除緩存的組件實例的 $destroy 方法,最後設置 vnode.data.keepAlive = true。此外keep-alive還會經過watch檢測傳入的include 和 exclude 的變化,對緩存作處理即對 cache 作遍歷,發現緩存的節點名稱和新的規則沒有匹配上的時候,就把這個緩存節點從緩存中摘除。 keep-alive的子組件生成的vnode爲:
{
asyncFactory: undefined
asyncMeta: undefined,
children: undefined,
componentInstance: undefined,
componentOptions: {
Ctor: ƒ,
propsData: undefined,
listeners: undefined,
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,
}
複製代碼
接着在渲染階段的createElm函數調用createComponent, 會對view1組件進行初始化並進行編譯模板生成render函數
(function anonymous(
) {
with(this){return _c('div',[_v("view component1")])}
})
複製代碼
最後是渲染過程。
在最後的渲染階段,createElm函數執行createComponent,會觸發componentVNodeHooks中的init鉤子,初次渲染vnode.componentInstance爲undefined,vnode.data.keepAlive設置了爲true,因此會進入else,走正常的mount流程:
// 前面設置了keep-alive屬性爲true,故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 {
// 將動態組件view1掛載到父級(非keep-alive組件)
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
// 子節點掛載到父組件
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
}
複製代碼
逐級掛載,最後渲染到頁面。
當一個組件切換到另外一個組件時,在patch過程當中會對比新舊vnode以及它們的子節點,而keep-alive組件的更新,首先在組件patchVnode過程當中,一個元素即將被修復時會執行prepatch鉤子函數:
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode);
}
複製代碼
即
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
);
},
複製代碼
裏面的關鍵代碼就是執行 updateChildComponent( child, options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children );
該函數的核心代碼:
複製代碼
// renderChildren爲最新的子組件[VNode],vm.$options._renderChildren表示老的子組件[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();
}
複製代碼
resolveSlots將新的子組件的VNode賦值給vm.$slots,即
vm.$slots = {
default: [VNode]
}
複製代碼
再進行強制更新,從新渲染
Vue.prototype.$forceUpdate = function () {
var vm = this;
if (vm._watcher) {
vm._watcher.update();
}
};
複製代碼
再次執行到createComponent函數時
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
// 更新時,isReactivated爲true
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
}
}
}
複製代碼
其中data爲:
{
hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ}
keepAlive: true,
on: undefined,
tag: "component",
}
複製代碼
上面的i(vnode, false /* hydrating */)
,執行
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// 更新過程的patch
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
}
}
複製代碼
在執行 init 鉤子函數的時候不會再執行組件的 mount 過程,回到createComponent函數,在 isReactivated 爲 true 的狀況下會執行 reactivateComponent 方法
function reactivateComponent (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); } 複製代碼
把緩存的 DOM 對象直接插入到目標元素。