《Vue.js实战》章七 组件——标签页组件:思路详解


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

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


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM