上一节最初略微提到了Vue
内置组件的相干内容,从这一节开始,将会对某个具体的内置组件进行剖析。首先是keep-alive
,它是咱们日常开发中常常应用的组件,咱们在不同组件间切换时,常常要求放弃组件的状态,以防止反复渲染组件造成的性能损耗,而keep-alive
常常和上一节介绍的动静组件联合起来应用。因为内容过多,keep-alive
的源码剖析将分为高低两局部,这一节次要围绕keep-alive
的首次渲染开展。
13.1 根本用法
keep-alive
的应用只须要在动静组件的最外层增加标签即可。
<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
。
13.2 从模板编译到生成vnode
依照以往剖析的教训,咱们会从模板的解析开始说起,第一个疑难便是:内置组件和一般组件在编译过程有区别吗?答案是没有的,不论是内置的还是用户定义组件,实质上组件在模板编译成render
函数的解决形式是统一的,这里的细节不开展剖析,有纳闷的能够参考前几节的原理剖析。最终针对keep-alive
的render
函数的后果如下:
with(this){···_c('keep-alive',{attrs:{"include":"child2"}},[_c(chooseTabs,{tag:"component"})],1)}
有了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; } }}
13.3 首次渲染
keep-alive
之所以特地,是因为它不会反复渲染雷同的组件,只会利用首次渲染保留的缓存去更新节点。所以为了全面理解它的实现原理,咱们须要从keep-alive
的首次渲染开始说起。
13.3.1 流程
和渲染一般组件雷同的是,Vue
会拿到后面生成的Vnode
对象执行实在节点创立的过程,也就是相熟的patch
过程,patch
执行阶段会调用createElm
创立实在dom
,在创立节点途中,keep-alive
的vnode
对象会被认定是一个组件Vnode
,因而针对组件Vnode
又会执行createComponent
函数,它会对keep-alive
组件进行初始化和实例化。
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
过程做了什么操作。
// 组件外部钩子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) }
13.3.2 内置组件选项
咱们在应用组件的时候常常利用对象的模式定义组件选项,包含data,method,computed
等,并在父组件或根组件中注册。keep-alive
同样遵循这个情理,内置两字也阐明了keep-alive
是在Vue
源码中内置好的选项配置,并且也曾经注册到全局。
// 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
解决是最优的抉择。
参考Vue3源码视频解说:进入学习
13.3.3 缓存vnode
还是先回到流程图的剖析。下面说到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}// matchesfunction 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
- 将曾经缓存的
13.3.4 实在节点的保留
咱们再回到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
进行了缓存。
13.4 形象组件
这一节的最初顺便提一下上文提到的形象组件的概念。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
又会有哪些魔法的存在呢,之前留下的缓存优化又是什么?这些都会在下一大节一一解开。