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里
}
writer by:大沙漠 QQ:22969969
insert会调用原生的insertBefore或者appendChild这去插入DOM,最后返回到patch()函数内,就把之前的B组件从DOM树中移除,并执行相关生命周期函数。
Vue.js 源码分析(三十一) 高级应用 keep-alive 组件 详解的更多相关文章
- Vue.js 源码分析(三十) 高级应用 函数式组件 详解
函数式组件比较特殊,也非常的灵活,它可以根据传入该组件的内容动态的渲染成任意想要的节点,在一些比较复杂的高级组件里用到,比如Vue-router里的<router-view>组件就是一个函 ...
- Vue.js 源码分析(二十二) 指令篇 v-model指令详解
Vue.js提供了v-model指令用于双向数据绑定,比如在输入框上使用时,输入的内容会事实映射到绑定的数据上,绑定的数据又可以显示在页面里,数据显示的过程是自动完成的. v-model本质上不过是语 ...
- Vue.js 源码分析(六) 基础篇 计算属性 computed 属性详解
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的.在模板中放入太多的逻辑会让模板过重且难以维护,比如: <div id="example">{{ messag ...
- Vue.js 源码分析(二十七) 高级应用 异步组件 详解
当我们的项目足够大,使用的组件就会很多,此时如果一次性加载所有的组件是比较花费时间的.一开始就把所有的组件都加载是没必要的一笔开销,此时可以用异步组件来优化一下. 异步组件简单的说就是只有等到在页面里 ...
- Vue.js 源码分析(三) 基础篇 模板渲染 el、emplate、render属性详解
Vue有三个属性和模板有关,官网上是这样解释的: el ;提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标 template ;一个字符串模板作为 Vue 实例的标识使用.模板将会 ...
- Vue.js 源码分析(二十一) 指令篇 v-pre指令详解
该指令会跳过所在元素和它的子元素的编译过程,也就是把这个节点及其子节点当作一个静态节点来处理,例如: <!DOCTYPE html> <html lang="en" ...
- Vue.js 源码分析(三十二) 总结
第一次写博客,坚持了一个多月时间,Vue源码分析基本分析完了,回过头也看也漏了一些地方,比如双向绑定里的观察者模式,也可以说是订阅者模式,也就是Vue里的Dep.Watcher等这些函数的作用,网上搜 ...
- jQuery 源码分析(二十一) DOM操作模块 删除元素 详解
本节说一下DOM操作模块里的删除元素模块,该模块用于删除DOM里的某个节点,也可以理解为将该节点从DOM树中卸载掉,如果该节点有绑定事件,我们可以选择保留或删除这些事件,删除元素的接口有如下三个: e ...
- Vue.js 源码分析(一) 代码结构
关于Vue vue是一个兴起的前端js库,是一个精简的MVVM.MVVM模式是由经典的软件架构MVC衍生来的,当View(视图层)变化时,会自动更新到ViewModel(视图模型),反之亦然,View ...
随机推荐
- HTTP/2 新特性总结
我在想了解HTTP/2的时候,查阅了很多资料,发现这篇很好,是外国的文章.我翻译过来,加入自己的一点理解. HTTP/2 更简单,高效,强大.它在传输层解决了以前我们HTTP1.x中一直存在的问题.使 ...
- 解决 layui 弹出层(弹框)一闪而过就消失的问题 (转载)
转载: 原文链接:https://blog.csdn.net/qq_20594019/article/details/83956532 本人遇到问题:使用layer.open()弹出页面层,出现弹框闪 ...
- RAID 2.0 技术(块虚拟化技术)
RAID 2.0 技术(块虚拟化技术) RAID 2.0 技术(块虚拟化技术),该技术将物理的存储空间划分为若干小粒度数据块,这些小粒度的数据块均匀的分布在存储池中所有的硬盘上,然后这些小粒度的数据块 ...
- 【linux】切换到root用户,并重置root用户密码
1.切换当前用户 到 root用户 sudo -i 2.重置root用户密码 sudo passwd root
- MySql配置主从模式 Last_IO_Error: Fatal error: The slave I/O thread stops because master and slave have equal MySQL server UUIDs; these UUIDs must be different for replication to work.
今天在学习MyCat环境搭建的时候,在配置MySql的主从模式,发现slave在配置完毕后,配置的内容全部正确的情况下,报错了? Last_IO_Error: Fatal error: The sla ...
- PlayJava Day006
今日所学: /* 2019.08.19开始学习,此为补档. */ 构造方法没有返回值(即return为空). this:实例(对象)的引用. JVM:①static方法区:存静态数据 ②栈区:引用 ...
- Java生鲜电商平台-生鲜供应链(采购管理)
Java生鲜电商平台-生鲜供应链(采购管理) 在生鲜供应链系统中采购中心这一模块,它是电商公司管理采购的模块,包含供应商管理,采购订单管理,采购商品管理,在该模块中采购订单是采购中心的核心模块.在其他 ...
- CAD画三维图怎么渲染?一分钟教你快速操作
从事过CAD相关工作的都知道,CAD绘制的方式有二维平面图以及三维图形,三维图形,画三维图方式也是比较简单的.那当然三维图画完后一般还需要进行渲染操作,步骤也是比较简洁的.下面就来给大家操作一下CAD ...
- Dynamics CRM定制子网格添加按钮实例之二:调试代码、打开Web资源及获取选择的记录
关注本人微信和易信公众号: 微软动态CRM专家罗勇 ,回复222或者20160501可方便获取本文,同时可以在第一间得到我发布的最新的博文信息,follow me!我的网站是 www.luoyong. ...
- 【转载】每个 Android 开发者必须知道的消息机制问题总结
Android的消息机制几乎是面试必问的话题,当然也并不是因为面试,而去学习,更重要的是它在Android的开发中是必不可少的,占着举足轻重的地位,所以弄懂它是很有必要的.下面就来说说最基本的东西. ...