Vue.js 源码分析(三十一) 高级应用 keep-alive 组件 详解
当使用is特性切换不同的组件时,每次都会重新生成组件vue实例并生成对应的vnode进行渲染,这样是比较花费性能的,而且切换重新显示时数据又会初始化,例如:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>document</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script> </head> <body> <div id="app"> <button @click="currentcomp=currentcomp=='a'?'b':'a'">切换</button> <!--动态组件--> <component :is="currentcomp"/> </div> <script> with(vue.config){productiontip=false;devtools=false;} var app = new vue({ el: '#app', components:{ a:{ template:"<div><input type='text'/></div>", name:'a', mounted:function(){console.log('comp a mounted');} }, b:{ template:"<div>b组件</div>", name:'b', mounted:function(){console.log('comp b mounted');} } }, data:{ currentcomp:"a" } }) </script> </body> </html>
渲染结果为:
控制台输出:
当我们在输入框输入内容后再点击切换将切换到b组件后控制台输出:
然后再次点击切换,将显示a组件,此时控制台输出:
渲染出的a组件内容是空白的,我们之前在输入框输入的内容将没有了,这是因为使用is特性切换不同的组件时,每次都会重新生成组件vue实例并生成对应的vnode进行渲染,数据会丢失的
解决办法是可以用kepp-alive组件对子组件内的组件实例进行缓存,子组件激活时将不会再创建一个组件实例,而是从缓存里拿到组件实例,直接挂载即可,
使用keep-alive组件时,可以给该组件传递以下特性:
include ;只有名称匹配的组件会被缓存 ;只可以是字符串数组、字符串(以逗号分隔,分隔后每个内容就是要缓存的组件名)、正则表达式
exclude ;任何名称匹配的组件都不会被缓存 ;只可以是字符串数组、字符串(以逗号分隔,分隔后每个内容就是要缓存的组件名)、正则表达式
max ;数字。最多可以缓存多少组件实例
keep-alive对应的子组件有两个生命周期函数,这两个生命周期是keep-alive特有的,如下:
activated ;该子组件被激活时调用
deactivated ;该子组件被停用时调用
例如:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>document</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script> </head> <body> <div id="app"> <button @click="currentcomp=currentcomp=='a'?'b':'a'">切换</button> <keep-alive> <component :is="currentcomp"/> </keep-alive> </div> <script> with(vue.config){productiontip=false;devtools=false;} var app = new vue({ el: '#app', components:{ a:{ template:"<div><input type='text'/></div>", name:'a', mounted:function(){console.log('comp a mounted');}, //挂载事件 activated:function(){console.log("comp a activated");}, //激活时的事件,kepp-alive独有的生命周期函数 deactivated:function(){console.log("comp a deactivated");} //停用时的事件,kepp-alive独有的生命周期函数 }, b:{ template:"<div>b组件</div>", name:'b', mounted:function(){console.log('comp b mounted');}, activated:function(){console.log("comp b activated");}, deactivated:function(){console.log("comp b deactivated");} } }, data:{ currentcomp:"a" } }) </script> </body> </html>
这样组件在切换时之前的数据就不会丢失了。
源码分析
对于keep-alive来说,是通过initglobalapi()函数注册的,如下:
var builtincomponents = { //第5059行,keppalive组件的定义 keepalive: keepalive }
function initglobalapi (vue) { //第5015行 /**/ extend(vue.options.components, builtincomponents); //第5051行 /**/ }
keep-alive组件的定义如下:
var keepalive = { //第4928行 name: 'keep-alive', abstract: true, props: { include: patterntypes, exclude: patterntypes, max: [string, number] }, created: function created () { //创建时的周期函数 this.cache = object.create(null); //用于缓存keepalive的vnode this.keys = []; //设置this.keys为空数组 }, destroyed: function destroyed () { //销毁生命周期 var this$1 = this; for (var key in this$1.cache) { prunecacheentry(this$1.cache, key, this$1.keys); } }, mounted: function mounted () { //挂载时的生命周期函数 var this$1 = this; this.$watch('include', function (val) { //监视include的变化 prunecache(this$1, function (name) { return matches(val, name); }); }); this.$watch('exclude', function (val) { //监视exclude的变化 prunecache(this$1, function (name) { return !matches(val, name); }); }); }, render: function render () { //render函数 /**/ } }
keep-alive也是一个抽象组件(abstract属性为true),mounted挂载时会监视include和exclude的变化,也就是说程序运行时可以通过修改include或exclude来对keep-alive里缓存的子组件进行移除操作。
keep-alive组件的render函数如下:
render: function render () { //第4926行 keepalive组件的render函数 var slot = this.$slots.default; //获取所有的子节点,是个vnode数组 var vnode = getfirstcomponentchild(slot); //拿到第一个组件vnode var componentoptions = vnode && vnode.componentoptions; //该组件的配置信息 if (componentoptions) { // check pattern var name = getcomponentname(componentoptions); //获取组件名称,优先获取name属性,如果没有则获取tag名称 var ref = this; //当前keppalive组件的vue实例 var include = ref.include; //获取include属性 var exclude = ref.exclude; //获取exclude属性 if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) //执行matches进行匹配,如果该组件不满足条件 ) { return vnode //则直接返回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; //为子组件定义一个唯一的key值 如果该子组件没有定义key则拼凑一个,值为该组件对应的vue实例的cid::tag,例如:1::a 同一个构造函数可以注册为不同的组件,所以单凭一个cid作为凭证是不够的 if (cache[key]) { //如果该组件被缓存了 vnode.componentinstance = cache[key].componentinstance; //直接将该组件的实例保存到vnode.componentinstance里面 // make current key freshest remove(keys, key); keys.push(key); } else { //如果当前组件没有被缓存 cache[key] = vnode; //先将vnode保存到缓存cache里 keys.push(key); //然后将key保存到keys里 // prune oldest entry if (this.max && keys.length > parseint(this.max)) { //如果指定了max且当前的keys里存储的长度大于this.max prunecacheentry(cache, keys[0], keys, this._vnode); //则移除keys[0],这是最不常用的子组件 } } vnode.data.keepalive = true; //设置vnode.data.keepalive为true,即设置一个标记 } return vnode || (slot && slot[0]) //最后返回vnode(即第一个组件子节点) }
matches用于匹配传给kepp-alive的include或exclude特性是否匹配,如下:
function matches (pattern, name) { //第4885行 //查看name这个组件是否匹配pattern if (array.isarray(pattern)) { //pattern可以是数组格式 return pattern.indexof(name) > -1 } else if (typeof pattern === 'string') { //也可以是字符串,用逗号分隔 return pattern.split(',').indexof(name) > -1 } else if (isregexp(pattern)) { //也可以是正则表达式 return pattern.test(name) } /* istanbul ignore next */ return false }
初次渲染时,keep-alive下的组件和普通组件是没有区别的,当一个组件从被激活变为激活状态时,和keep-alive相关的逻辑如下:
执行patch()将vnode渲染成真实节点时会执行createelm()函数,又会优先执行createcomponent创建组件实例,如下:
function createcomponent (vnode, insertedvnodequeue, parentelm, refelm) { //第5589行 创建组件节点 var i = vnode.data; //获取vnode的data属性 if (isdef(i)) { //如果存在data属性(组件vnode肯定存在这个属性,普通vnode有可能存在) var isreactivated = isdef(vnode.componentinstance) && i.keepalive; //是否为激活操作 如果vnode.componentinstance为true(组件实例存在)且存在keepalive属性则表示为keepalive组件 if (isdef(i = i.hook) && isdef(i = i.init)) { i(vnode, false /* hydrating */, parentelm, refelm); //执行组件的init钩子函数 } // 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); //将子组件的vnode push到insertedvnodequeue里面, if (istrue(isreactivated)) { //如果是keep-alive激活的状态 reactivatecomponent(vnode, insertedvnodequeue, parentelm, refelm); //执行reactivatecomponent()函数 } return true } } }
init是组件的钩子函数,用于创建组件的实例,如下:
init: function init ( //第4109行 组件的安装 vnode, hydrating, parentelm, refelm ) { if ( vnode.componentinstance && !vnode.componentinstance._isdestroyed && vnode.data.keepalive ) { //如果vnode.componentinstance和vnode.data.keepalive都存在,则表示是一个keep-alive组件的激活状态 // kept-alive components, treat as a patch var mountednode = vnode; // work around flow componentvnodehooks.prepatch(mountednode, mountednode); //执行该组件的prepatch方法 } else { var child = vnode.componentinstance = createcomponentinstanceforvnode( vnode, activeinstance, parentelm, refelm ); child.$mount(hydrating ? vnode.elm : undefined, hydrating); } },
对于keep-alive子组件的激活过程来说,它是不会调用createcomponentinstanceforvnode去创建一个新的组件实例的,而是直接从vnode的componentinstance拿到组件实例即可
回到createcomponent()函数,最后会执行reactivatecomponent()函数,该函数就比较简单了,就是将子组件vnode.elm插入到dom中,如下:
function reactivatecomponent (vnode, insertedvnodequeue, parentelm, refelm) { //第5628行 激活一个组件 var i; // hack for #4339: a reactivated component with inner transition // does not trigger because the inner node's created hooks are not called // again. it's not ideal to involve module-specific logic in here but // there doesn't seem to be a better way to do it. var innernode = vnode; while (innernode.componentinstance) { innernode = innernode.componentinstance._vnode; if (isdef(i = innernode.data) && isdef(i = i.transition)) { for (i = 0; i < cbs.activate.length; ++i) { cbs.activate[i](emptynode, innernode); } insertedvnodequeue.push(innernode); break } } // unlike a newly created component, // a reactivated keep-alive component doesn't insert itself insert(parentelm, vnode.elm, refelm); //调用insert将vnode.elm插入到parentelm里 }
insert会调用原生的insertbefore或者appendchild这去插入dom,最后返回到patch()函数内,就把之前的b组件从dom树中移除,并执行相关生命周期函数。
上一篇: 飞聊停运多闪并入抖音,字节社交按下暂停键
推荐阅读
-
Vue.js 源码分析(二十五) 高级应用 插槽 详解
-
Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解
-
Vue.js 源码分析(十二) 基础篇 组件详解
-
Vue.js 源码分析(二十四) 高级应用 自定义指令详解
-
Vue.js 源码分析(三十一) 高级应用 keep-alive 组件 详解
-
Vue.js 源码分析(十三) 基础篇 组件 props属性详解
-
Vue.js 源码分析(二十六) 高级应用 作用域插槽 详解
-
Vue.js 源码分析(二十九) 高级应用 transition-group组件 详解
-
Vue.js 源码分析(二十八) 高级应用 transition组件 详解
-
Vue.js 源码分析(二十七) 高级应用 异步组件 详解