先上带有部分注释的全部代码

<!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>

先来明确思路:

Swift 标签组件 标签页组件_初始化

务必明确插槽slot的作用与组件渲染方式。

 

Swift 标签组件 标签页组件_标签页_02

以下为pane组件(建议将每一点结合起来看)

1.name:'pane',这里的name此时不是为了在组件内部递归调用自身,而是作为唯一标识,既可以专门设置一个prop:name,也可以不做设置在后面用index代替

2.show:true,由pane配合v-show以及name控制标签页的显示与隐藏,注意这一步操作是由tabs组件来进行的。

3.props的传入过滤,用户是可以动态调整prop:label的!因此如果在pane初始化或label更新时都需要通知父组件更新状态(即导航栏)

4.子组件的updateNav实际上是调用了父组件的这个同名方法

Swift 标签组件 标签页组件_标签页_03

5.同4,watch与mounted中的updateNav方法也是调用父组件,作用域亦在父组件。

 

以下为tabs组件,分析并不从上到下。

6.先来看上面的updateNav()方法到底做了什么?

Swift 标签组件 标签页组件_标签页_04

先将tabs的子组件列表navList初始化,然后调用getTabs()方法,得到所有的pane组件,注意filter、$options。

然后将组件的label值与name推入数组,如果我们没有给出name(如下图),那么就以索引值作为name。

Swift 标签组件 标签页组件_初始化_05

在遍历过程中,遍历到第一个组件时,才会触发if(index === 0){...},此时这里的index是作为参数传入的索引值(无论有无指定name),

如果没有指定currentValue,那么此时将currentValue指定为这个pane的name或索引值。

再结束前还会触发一次updateStatus方法,见下文。

7.currentValue是什么??首先,我们知道这个变量存储着当前显示的标签页,上一点的操作实际上就是确保在初始化时有一个默认标签页显示。其他暂且不管。

.

Swift 标签组件 标签页组件_Swift 标签组件_06

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自定义事件,

Swift 标签组件 标签页组件_初始化_07

我们都知道v-model的本质是一个语法糖,activeKey的值会被动态修改为name值。(activeKey存在于tabs的data中,这一修改是双向的,便于进行其他操作)。

10.

Swift 标签组件 标签页组件_初始化_08

updateStatus()方法,实际上是负责控制标签页显示与隐藏的方法。

再次遍历获取pane组件,并将结果再次遍历(注意,两次遍历分别的对象),如果tab.name === _this.currentValue,就可以露脸,否则隐藏。

这一步中实际依据是否设置value有不同的处理结果,有可能只规定了value的类型而没有规定default,currentValue为null。

注意!可能会都感到疑惑这个value的值在哪?

Swift 标签组件 标签页组件_标签页_09

事实上value值就是那个activeKey,还记得v-model这个语法糖吗?

Swift 标签组件 标签页组件_初始化_10

注意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,更改显示状态。