先上带有部分注释的全部代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="Vue.2.6.10.js"></script>
</head>
<body>
<!-- 需求分析:
1.每个标签页的主体内容(即html结构、文本、图片等)应当是由使用组件的父级控制的,这部分可以作为一个slot,
slot的数量决定了标签切换按钮的数量。点击每个按钮时,另外的标签对应的slot应该被隐藏掉。第一种想法,在slot里写三个div,再
根据需要隐藏和显示。更好的办法应该是让组件帮忙处理这部分逻辑,我们只需要聚焦slot内容本身。这种情况下,再定义一个子组件pane,
嵌套在标签页组件tabs里,把业务代码放在pane的slot内,然后各个pane都作为整个tabs的组件。
tabs和pane两个组件是分离的,但tabs上的标题应该由pane组件来定义,又因为slot写在pane里,所以应该在组件初始化(或标签标题动态改变)时,tabs从
当前的pane里获取标题并保存起来。-->
<div id="app" v-cloak>
<tabs v-model="activeKey">
<pane label="标签一" name="1">
标签一内容
</pane>
<pane label="标签二" name="2">
标签二内容
</pane>
<pane label="标签三" name="3">
标签三内容
</pane>
</tabs>
</div>
<script>
Vue.component('pane',{
name:"pane",
template:'\
<div class="pane" v-show="show">\
<slot></slot>\
</div>',
data() {
return {
show:true//pane需要控制标签页内容的显示与隐藏,配合v-show
//点击到这个pane对应的标签页按钮时,这个pane的show值才设置为true
//既然这样的话就应该有唯一标识的值来标识这个pane,可以设置一个prop:name让用户来设置,但不是必必需的,默认从0开始
//这一步操作由pane执行,pane本身并不知道自己是第几个。
//还有prop:label,tabs组件需要将它显示在标签栏标题中
}
},
props:{
name:{
type:String
},
label:{
type:String,
default:''
}
},//prop:label是用户可以动态调整的,所以在pane初始化、label更新时都需要通知父组件也更新,
//可以直接通过this.$parent来访问tabs组件的实例来调用它的方法更新标题
methods: {
updateNav(){
this.$parent.updateNav();//调用tabs的方法updateNav
}
},
watch: {
label(){
this.updateNav();
}
},
mounted() {
this.updateNav();//在pane初始化时调用一遍tabs的方法updateNav,同时监听prop:label,在其更新时也调用
},
})
Vue.component('tabs',{
template:'\
<div class="tabs">\
<div class="tabs-bar">\
<!--标签页的标题,需要使用v-for-->\
<div \
:class="tabCls(item)"\
v-for="(item,index) in navList"\
@click="handleChange(index)">\
{{ item.label }}\
</div>\
</div>\
<div class="tabs-content">\
<!--这里的slot即是嵌套的pane组件-->\
<slot></slot>\
</div>\
</div>',
props:{
value:{
type:[String,Number]
}
},
data() {
return {
//由于不能修改value于是自己复制一份维护
currentValue:this.value,
//用于渲染tabs的标题
navList:[]
}
},
methods:{
tabCls:function(item){
return [
'tabs-tab',
{
//给当前选中的div加一个class
'tabs-tab-active':item.name === this.currentValue
}
]
},
//点击tab标题时触发
handleChange:function(index){
var nav = this.navList[index];
var name = nav.name;
//改变当前选中的tab,并触发下面的watch
this.currentValue = name;
//更新value
this.$emit('input',name);
//触发一个自定义事件供父级使用
this.$emit('on-click',name);
},
getTabs(){
//通过遍历子组件得到所有panes组件
return this.$children.filter(function(item){
return item.$options.name === 'pane'
})
},
updateNav(){
this.navList = [];
//设置对this的引用,在function回调里this指向的并不是Vue实例
var _this = this;
this.getTabs().forEach(function(pane,index){
_this.navList.push({
label:pane.label,
name:pane.name || index
});
//如果没有给pane设置name就默认设置为索引
if(!pane.name){
pane.name = index;
}//设置当前选中的tabs的索引
if(index === 0){
if(!_this.currentValue){
_this.currentValue = pane.name || index;
}
}
});
this.updateStatus();
},
updateStatus(){
var tabs = this.getTabs();
//再次遍历pane组件,不过这是为了将当前选中的
//tab对应的pane组件内容显示出来,并将没有选中的隐藏掉
//在上一步中可能需要我们来设置currentValue来标识当前选中项的name(只有在用户没有设置value时才会自动设置)
var _this = this;
//显示当前选中的tab对应的pane组件,隐藏没有选中的
tabs.forEach(function(tab){
return tab.show = tab.name === _this.currentValue;
// return tab.show = (tab.name === _this.currentValue);
})
}
},
watch:{
value:function(val){
this.currentValue = val;
},
currentValue:function(){
//在当前选中的tabs发生变化时更新pane显示状态
this.updateStatus();
}
}
});
var app = new Vue({
el:"#app",
data:{
activeKey:'1'
}
});
</script>
</body>
</html>
先来明确思路:
务必明确插槽slot的作用与组件渲染方式。
以下为pane组件(建议将每一点结合起来看)
1.name:'pane',这里的name此时不是为了在组件内部递归调用自身,而是作为唯一标识,既可以专门设置一个prop:name,也可以不做设置在后面用index代替
2.show:true,由pane配合v-show以及name控制标签页的显示与隐藏,注意这一步操作是由tabs组件来进行的。
3.props的传入过滤,用户是可以动态调整prop:label的!因此如果在pane初始化或label更新时都需要通知父组件更新状态(即导航栏)
4.子组件的updateNav实际上是调用了父组件的这个同名方法
5.同4,watch与mounted中的updateNav方法也是调用父组件,作用域亦在父组件。
以下为tabs组件,分析并不从上到下。
6.先来看上面的updateNav()方法到底做了什么?
先将tabs的子组件列表navList初始化,然后调用getTabs()方法,得到所有的pane组件,注意filter、$options。
然后将组件的label值与name推入数组,如果我们没有给出name(如下图),那么就以索引值作为name。
在遍历过程中,遍历到第一个组件时,才会触发if(index === 0){...},此时这里的index是作为参数传入的索引值(无论有无指定name),
如果没有指定currentValue,那么此时将currentValue指定为这个pane的name或索引值。
再结束前还会触发一次updateStatus方法,见下文。
7.currentValue是什么??首先,我们知道这个变量存储着当前显示的标签页,上一点的操作实际上就是确保在初始化时有一个默认标签页显示。其他暂且不管。
.
8.先来看看模板中的这些是什么意思:
首先得明确v-for在这里会生成navList中的各个pane组件,并为每一个div填充这些数据。
tabCls的返回值是一个字符串数组,必定包含tabs-tab,如果这个标签页当前是被选中的,那么tabs的currentValue值即等于当前标签页的name,还会多获得一个active的类名。
9.当点击这个标签时,触发handleChange函数,并传入当前这个标签的index值,在这个函数里会完成以下操作:
1.先将当前标签以及name属性存储起来(nav,name)
2.将tabs组件的currentValue修改为name值
3.触发父组件的input事件与on-click自定义事件,
我们都知道v-model的本质是一个语法糖,activeKey的值会被动态修改为name值。(activeKey存在于tabs的data中,这一修改是双向的,便于进行其他操作)。
10.
updateStatus()方法,实际上是负责控制标签页显示与隐藏的方法。
再次遍历获取pane组件,并将结果再次遍历(注意,两次遍历分别的对象),如果tab.name === _this.currentValue,就可以露脸,否则隐藏。
这一步中实际依据是否设置value有不同的处理结果,有可能只规定了value的类型而没有规定default,currentValue为null。
注意!可能会都感到疑惑这个value的值在哪?
事实上value值就是那个activeKey,还记得v-model这个语法糖吗?
注意v-bind:value=“dataA”这一行!!!!
紧接着由于没有指定value,currentValue会被自动设置(第六点),为1(因为本例中有给prop指定name,否则就是第一个索引值0)。
如果指定了value值(本例value值即为activeKey),即指定要求初始化时被选中的标签页,那么pane初始化时,currentValue被指定为value值(以3为例),在tabs的updateNav()方法中,不会执行if(index === 0){...}中的改变,紧接着执行updateStatus(),name为3的pane组件会被显示出来。
12.假定一个完整的执行流程:
初始activeKey=2——v-model=2——value=2——cV=2——初始化,updateNav()——填充navList——执行updateStatus,将第二个组件显示出来。
点击第一个标签页——触发handleChange,改变cV值,更新value——tabs中的watch监控到value与cV值变化,将cV值修改为value值,同时再次执行updateStatus,更改显示状态。