上一节最后稍微提到了
Vue
内置组件的相关内容,从这一节开始,将会对某个具体的内置组件进行分析。首先是keep-alive
,它是咱们平常开发中常用的组件,咱们在不一样组件间切换时,常常要求保持组件的状态,以免重复渲染组件形成的性能损耗,而keep-alive
常常和上一节介绍的动态组件结合起来使用。因为内容过多,keep-alive
的源码分析将分为上下两部分,这一节主要围绕keep-alive
的首次渲染展开。vue
keep-alive
的使用只须要在动态组件的最外层添加标签便可。node
<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++
}
},
}
var child2 = {
template: '<div>child2</div>'
}
var vm = new Vue({
el: '#app',
components: {
child1,
child2,
},
data() {
return {
chooseTabs: 'child1',
}
},
methods: {
changeTabs(tab) {
this.chooseTabs = tab;
}
}
})
复制代码
简单的结果以下,动态组件在child1,child2
之间来回切换,当第二次切到child1
时,child1
保留着原来的数据状态,num = 5
。react
按照以往分析的经验,咱们会从模板的解析开始提及,第一个疑问即是:内置组件和普通组件在编译过程有区别吗?答案是没有的,不论是内置的仍是用户定义组件,本质上组件在模板编译成render
函数的处理方式是一致的,这里的细节不展开分析,有疑惑的能够参考前几节的原理分析。最终针对keep-alive
的render
函数的结果以下:算法
with(this){···_c('keep-alive',{attrs:{"include":"child2"}},[_c(chooseTabs,{tag:"component"})],1)}
api
有了render
函数,接下来从子开始到父会执行生成Vnode
对象的过程,_c('keep-alive'···)
的处理,会执行createElement
生成组件Vnode
,其中因为keep-alive
是组件,因此会调用createComponent
函数去建立子组件Vnode
,createComponent
以前也有分析过,这个环节和建立普通组件Vnode
不一样之处在于,keep-alive
的Vnode
会剔除多余的属性内容,因为keep-alive
除了slot
属性以外,其余属性在组件内部并无意义,例如class
样式,<keep-alive clas="test"></keep-alive>
等,因此在Vnode
层剔除掉多余的属性是有意义的。而<keep-alive slot="test">
的写法在2.6以上的版本也已经被废弃。(其中abstract
做为抽象组件的标志,以及其做用咱们后面会讲到)数组
// 建立子组件Vnode过程
function createComponent(Ctordata,context,children,tag) {
// abstract是内置组件(抽象组件)的标志
if (isTrue(Ctor.options.abstract)) {
// 只保留slot属性,其余标签属性都被移除,在vnode对象上再也不存在
var slot = data.slot;
data = {};
if (slot) {
data.slot = slot;
}
}
}
复制代码
keep-alive
之因此特别,是由于它不会重复渲染相同的组件,只会利用初次渲染保留的缓存去更新节点。因此为了全面了解它的实现原理,咱们须要从keep-alive
的首次渲染开始提及。缓存
为了理清楚流程,我大体画了一个流程图,流程图大体覆盖了初始渲染keep-alive
所执行的过程,接下来会照着这个过程进行源码分析。bash
和渲染普通组件相同的是,Vue
会拿到前面生成的Vnode
对象执行真实节点建立的过程,也就是熟悉的patch
过程,patch
执行阶段会调用createElm
建立真实dom
,在建立节点途中,keep-alive
的vnode
对象会被认定是一个组件Vnode
,所以针对组件Vnode
又会执行createComponent
函数,它会对keep-alive
组件进行初始化和实例化。app
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
// isReactivated用来判断组件是否缓存。
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 执行组件初始化的内部钩子 init
i(vnode, false /* hydrating */);
}
if (isDef(vnode.componentInstance)) {
// 其中一个做用是保留真实dom到vnode中
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
复制代码
keep-alive
组件会先调用内部钩子init
方法进行初始化操做,咱们先看看init
过程作了什么操做。dom
// 组件内部钩子
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 {
// 将组件实例赋值给vnode的componentInstance属性
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},
// 后面分析
prepatch: function() {}
}
复制代码
第一次执行,很明显组件vnode
没有componentInstance
属性,vnode.data.keepAlive
也没有值,因此会调用createComponentInstanceForVnode
方法进行组件实例化并将组件实例赋值给vnode
的componentInstance
属性, 最终执行组件实例的$mount
方法进行实例挂载。
createComponentInstanceForVnode
就是组件实例化的过程,而组件实例化从系列的第一篇就开始说了,无非就是一系列选项合并,初始化事件,生命周期等初始化操做。
function createComponentInstanceForVnode (vnode, parent) {
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
// 内联模板的处理,忽略这部分代码
···
// 执行vue子组件实例化
return new vnode.componentOptions.Ctor(options)
}
复制代码
咱们在使用组件的时候常常利用对象的形式定义组件选项,包括data,method,computed
等,并在父组件或根组件中注册。keep-alive
一样遵循这个道理,内置两字也说明了keep-alive
是在Vue
源码中内置好的选项配置,而且也已经注册到全局,这一部分的源码能够参考深刻剖析Vue源码 - Vue动态组件的概念,你会乱吗?小节末尾对内置组件构造器和注册过程的介绍。这一部分咱们重点关注一下keep-alive
的具体选项。
// keepalive组件选项
var KeepAlive = {
name: 'keep-alive',
// 抽象组件的标志
abstract: true,
// keep-alive容许使用的props
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created: function created () {
// 缓存组件vnode
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;
// 动态include和exclude
// 对include exclue的监听
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); });
});
},
// keep-alive的渲染函数
render: function render () {
// 拿到keep-alive下插槽的值
var slot = this.$slots.default;
// 第一个vnode节点
var vnode = getFirstComponentChild(slot);
// 拿到第一个组件实例
var componentOptions = vnode && vnode.componentOptions;
// keep-alive的第一个子组件实例存在
if (componentOptions) {
// check pattern
//拿到第一个vnode节点的name
var name = getComponentName(componentOptions);
var ref = this;
var include = ref.include;
var exclude = ref.exclude;
// 经过判断子组件是否知足缓存匹配
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
? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
: vnode.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
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
// 为缓存组件打上标志
vnode.data.keepAlive = true;
}
// 将渲染的vnode返回
return vnode || (slot && slot[0])
}
};
复制代码
keep-alive
选项跟咱们平时写的组件选项仍是基本相似的,惟一的不一样是keep-ailve
组件没有用template
而是使用render
函数。keep-alive
本质上只是存缓存和拿缓存的过程,并无实际的节点渲染,因此使用render
处理是最优的选择。
仍是先回到流程图的分析。上面说到keep-alive
在执行组件实例化以后会进行组件的挂载。而挂载$mount
又回到vm._render(),vm._update()
的过程。因为keep-alive
拥有render
函数,因此咱们能够直接将焦点放在render
函数的实现上。
keep-alive
下插槽的内容,也就是keep-alive
须要渲染的子组件,例子中是chil1 Vnode
对象,源码中对应getFirstComponentChild
函数function getFirstComponentChild (children) {
if (Array.isArray(children)) {
for (var i = 0; i < children.length; i++) {
var c = children[i];
// 组件实例存在,则返回,理论上返回第一个组件vnode
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c
}
}
}
}
复制代码
keep-alive
组件的使用过程当中,Vue
源码容许咱们是用include, exclude
来定义匹配条件,include
规定了只有名称匹配的组件才会被缓存,exclude
规定了任何名称匹配的组件都不会被缓存。更者,咱们可使用max
来限制能够缓存多少匹配实例,而为何要作数量的限制呢?咱们后文会提到。拿到子组件的实例后,咱们须要先进行是否知足匹配条件的判断,其中匹配的规则容许使用数组,字符串,正则的形式。
var include = ref.include;
var exclude = ref.exclude;
// 经过判断子组件是否知足缓存匹配
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
// matches
function matches (pattern, name) {
// 容许使用数组['child1', 'child2']
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
// 容许使用字符串 child1,child2
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
// 容许使用正则 /^child{1,2}$/g
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
复制代码
若是组件不知足缓存的要求,则直接返回组件的vnode
,不作任何处理,此时组件会进入正常的挂载环节。
render
函数执行的关键一步是缓存vnode
,因为是第一次执行render
函数,选项中的cache
和keys
数据都没有值,其中cache
是一个空对象,咱们将用它来缓存{ name: vnode }
枚举,而keys
咱们用来缓存组件名。 所以咱们在第一次渲染keep-alive
时,会将须要渲染的子组件vnode
进行缓存。cache[key] = vnode;
keys.push(key);
复制代码
vnode
打上标记, 并将子组件的Vnode
返回。 vnode.data.keepAlive = true
咱们再回到createComponent
的逻辑,以前提到createComponent
会先执行keep-alive
组件的初始化流程,也包括了子组件的挂载。而且咱们经过componentInstance
拿到了keep-alive
组件的实例,而接下来重要的一步是将真实的dom
保存再vnode
中。
function createComponent(vnode, insertedVnodeQueue) {
···
if (isDef(vnode.componentInstance)) {
// 其中一个做用是保留真实dom到vnode中
initComponent(vnode, insertedVnodeQueue);
// 将真实节点添加到父节点中
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
复制代码
insert
的源码不列举出来,它只是简单的调用操做dom
的api
,将子节点插入到父节点中,咱们能够重点看看initComponent
关键步骤的逻辑。
function initComponent() {
···
// vnode保留真实节点
vnode.elm = vnode.componentInstance.$el;
···
}
复制代码
所以,咱们很清晰的回到以前遗留下来的问题,为何keep-alive
须要一个max
来限制缓存组件的数量。缘由就是keep-alive
缓存的组件数据除了包括vnode
这一描述对象外,还保留着真实的dom
节点,而咱们知道真实节点对象是庞大的,因此大量保留缓存组件是耗费性能的。所以咱们须要严格控制缓存的组件数量,而在缓存策略上也须要作优化,这点咱们在下一篇文章也继续提到。
因为isReactivated
为false
,reactivateComponent
函数也不会执行。至此keep-alive
的初次渲染流程分析完毕。
若是忽略步骤的分析,只对初次渲染流程作一个总结:内置的keep-alive
组件,让子组件在第一次渲染的时候将vnode
和真实的elm
进行了缓存。
这一节的最后顺便提一下上文提到的抽象组件的概念。Vue
提供的内置组件都有一个描述组件类型的选项,这个选项就是{ astract: true }
,它代表了该组件是抽象组件。什么是抽象组件,为何要有这一类型的区别呢?我以为归根究底有两个方面的缘由。
dom
节点,而只是做为中间的数据过渡层处理,在keep-alive
中是对组件缓存的处理。initLifecycle
的代码。Vue.prototype._init = function() {
···
var vm = this;
initLifecycle(vm)
}
function initLifecycle (vm) {
var options = vm.$options;
var parent = options.parent;
if (parent && !options.abstract) {
// 若是有abstract属性,一直往上层寻找,直到不是抽象组件
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent;
}
parent.$children.push(vm);
}
···
}
复制代码
子组件在注册阶段会把父实例挂载到自身选项的parent
属性上,在initLifecycle
过程当中,会反向拿到parent
上的父组件vnode
,并为其$children
属性添加该子组件vnode
,若是在反向找父组件的过程当中,父组件拥有abstract
属性,便可断定该组件为抽象组件,此时利用parent
的链条往上寻找,直到组件不是抽象组件为止。initLifecycle
的处理,让每一个组件都能找到上层的父组件以及下层的子组件,使得组件之间造成一个紧密的关系树。
有了第一次的缓存处理,当第二次渲染组件时,
keep-alive
又会有哪些魔法的存在呢,以前留下的缓存优化又是什么?这些都会在下一小节一一解开。