Vue.js实战—组件详解

1.1 组件与复用

1.1.1 为什么使用组件

Vue.js的组件就是提高重用性,让代码可已复用。

1.1.2 组件的用法

<div id="app">
    <my-compoment></my-compoment>
</div>
<script>
    var Child = {
        template: '<div>局部注册组件的内容</div>'
    }
    
    var app = new Vue({
        el: '#app',
        components: {
            'my-compoment': Child
        }
    })
</script>

Vue组件的模板在某些情况下会受到HTML的限制,比如<table>内规定只允许<tr>、<td>、<th>等这些表格元素,所以在<table>内直接使用组件是无效的。这种情况下可以使用特殊的is属性来挂载组件,例如:

<div id="app">
    <table>
        <tbody is="my-component"></tbody>
    </table>
</div>
<script>
    Vue.component('my-component',{
        template: '<div>这里是组件的内容</div>'
    }) 

    var app = new Vue({
        el: '#app'
    })
</script>

tbody在渲染时,会被替换为组件的内容。常见的限制元素还有<ul>、<ol>、<select>。如果使用的是字符串模板,是不受限制的。

除了template选项外,组件中还可以像Vue实例那样使用其他的选项,比如data、computed、methods等。但在使用data时,和实例稍有区别,data必须是函数,然后将数据return出去,例如:

<div id="app">
    <my-compoment></my-compoment>
</div>
<script>
    Vue.component('my-component',{
        template: '<div>{{message}}</div>',
        data: function () {
            return {
                message: '组件内容'
            }
        }
    })

    var app = new Vue({
        el: '#app',
    })

1.2 使用props传递数据

1.2.1 基本用法

组件不仅仅是要把模板的内容进行复用,更重要的是组件间要进行通信。通常父组件的模板中包含子组件,父组件要正向地向子组件传递数据或参数,子组件接收到后根据参数的不同来渲染不同的内容或执行操作。这个正向传递数据的过程就是通过props来实现的。

在组件中,使用选项props来声明需要从父组件接收的数据,props的值可以是两种,一种是字符串数组,一种是对象。比如我们构造一个数组,接收一个来自父级的数据message,并把它在组件模板中渲染,例如:

<div id="app">
    <my-compoment message="来自父组件的数据"></my-compoment>
</div>
<script>
    Vue.component('my-component',{
        props: ['message'],
        template: '<div>{{message}}</div>'
    })

    var app = new Vue({
        el: '#app',
    })
</script>

props中声明的数据与组件data函数return的数据主要区别就是props的来自父级,而data中的是组件自己的数据,作用域是组件本身,这两种数据都是可以在模板template及计算属性computed和方法methods中使用的。上例的数据message就是通过props从父级传递过来的,在组件的自定义标签上直接写该props的名称,如果要传递多个数据,在props数组中添加项即可。

由于HTML特性不去区分大小写,当使用DOM模板时,驼峰命名的props名称要转为短横分隔命名。如果使用的是字符串模板仍然可以忽略这些限制。

1.2.2 单向数据流

Vue 2.x与Vue 1.x比较大的一个改变就是,Vue 2.x通过props传递数据是单向的了,也就是父组件数据变化时会传递给子组件,但是反过来不行。而在Vue 1.x里提供了.sync修饰符来支持双向绑定。之所以这样设计,是尽可能将父子组件解耦,避免子组件无意中修改了父组件的状态。

业务中经常遇到两种需要改变props的情况,一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件data内再声明一个数据,引用父组件的props,例如:

<div id="app">
    <my-compoment :init-count="1"></my-compoment>
</div>
<script>
    Vue.component('my-component',{
        props: ['initCount'],
        template: '<div>{{count}}</div>',
        data: function () {
            return {
                count: this.initCount
            }
        }
    })

    var app = new Vue({
        el: '#app',
    })
</script>

组件中声明了数据count,它在组件初始化时会获取来自父组件的initCount,之后就与之无关了,只用维护count,这样就可以避免直接操作initCount。

另一种情况就是prop作为需要被转变的原始值传入。这种情况用计算属性就可以了,例如:

<div id="app">
    <my-compoment :width="100"></my-compoment>
</div>
<script>
    Vue.component('my-component',{
        props: ['width'],
        template: '<div :style="style">组件内容</div>',
        computed: {
            style: function() {
                return {
                    width: this.width + 'px'  
                }
            }
        }
    })

    var app = new Vue({
        el: '#app',
    })
</script>

注意,在JavaScrpt中对象和数组都是引用类型,指向同一个内存空间,所以props是对象和数组时,在子组件内改变是会影响父组件的。

1.2.3 数据验证

除了数组外,props选项的值也可以是对象。当prop需要验证时,就需要对象写法。

一般当你的组件需要提供给别人使用时,推荐都进行数据验证,比如某个数据必须是数字类型,如果传入字符串,就会在控制台弹出警告。

以下是prop的示例:

   Vue.component('my-component',{
        props: {
            //必须是数字类型
            propA: Number,
            //必须是字符串或数字类型
            propB: [String, Number],
            //布尔值,如果没有定义,默认值就是true
            propC: {
                type: Boolean,
                default: true
            },
            //数字,而且是必传
            propD: {
                type: Number,
                required: true
            },
            //如果是数组或对象,默认值必须是一个函数来返回
            propE: {
                type: Array,
                default: function () {
                    return [];
                }
            },
            //自定义一个验证函数
            propF: {
                validator: function (value) {
                    return value > 10;
                }
            }
        }
    });

验证的type类型可以是:

·String
·Number
·Boolean
·Object
·Array
·Function

type也可以是一个自定义构造器,使用instandceof检测。

当prop验证失败时,在开发版本下会在控制台抛出一条警告。

1.3 组件通信

组件关系可分为父子组件通信、兄弟组件通信、跨级组件通信。

1.3.1 自定义事件

当子组件需要向父组件传递数据时,就要用到自定义事件。我们在介绍指令v-on时有提到,v-on除了监听DOM事件外,还可以用于组件之间的自定义事件。

子组件用$emit()来触发事件,父组件用$on()来监听子组件的事件。

父组件也可以直接在子组件的自定义标签上使用v-on来监听子组件触发的自定义事件,例如:

<div id="app">
    <p>总数:{{total}}</p>
    <my-compoment @increase="handleGetTotal"
                  @reduce="handleGetTotal"></my-compoment>
</div>
<script>
    Vue.component('my-component',{
        props: ['width'],
        template: '\
        <div>\
            <button @click="handleIncrease">+1</button>\
            <button @click="handleReduce">-1</button>\
        </div>',
        data: function () {
            return {
                counter: 0
            }
        },
        methods: {
            handleIncrease: function(){
                this.counter++;
                this.$emit('increase',this.counter);
            },
            handleReduce: function () {
                this.counter--;
                this.$emit('reduce',this.counter);
            }
        }
    })

    var app = new Vue({
        el: '#app',
        data: {
            total: 0
        },
        methods: {
            handleGetTotal: function (total) {
                this.total = total;
            }
        }
    })
</script>

上面的示例中,子组件有两个按钮,分别实现加1和减1的效果,在改变组件的data "counter"后,通过$emit()再把它传递给父组件,父组件用v-on:increase和v-on:reduce。$emit()方法的第一个参数是自定义时间的名称,例如示例的increase和reduce后面的参数都是要传递的数据,可以不填或填写多个。

除了用v-on在组件上监听自定义事件外,也可以监听DOM事件,这时可以用.native修饰符表示监听的是一个原生事件,监听的是该组件的根元素,例如:

  <my-compoment v-on:click.native="handleClick"></my-compoment>

1.3.2 使用v-model

Vue 2.x可以在自定义组件上使用v-model指令,例如:

<div id="app">
    <p>总数:{{total}}</p>
    <my-compoment v-model="total"></my-compoment>
</div>
<script>
    Vue.component('my-component',{
        props: ['width'],
        template: '<button @click="handleClick">+1</button>',
        data: function () {
            return {
                counter: 0
            }
        },
        methods: {
            handleClick: function () {
                this.counter++;
                this.$emit('input', this.counter);
            }
        }
    })

    var app = new Vue({
        el: '#app',
        data: {
            total: 0
        },
    })
</script>

仍然是点击按钮加1的效果,不过这次组件$emit()的事件名是特殊的input,在使用组件的父级,并没有在<my-component>上使用@input="handler",而是直接用了v-model绑定一个数据total,这也可以称作是一个语法糖。

v-model还可以用来创建自定义的表单输入组件,进行数据双向绑定,例如:

<div id="app">
    <p>总数:{{total}}</p>
    <my-compoment v-model="total"></my-compoment>
    <button @click="handleReduce">-1</button>
</div>
<script>
    Vue.component('my-component',{
        props: ['value'],
        template: '<input :value="value" @input="updateValue">',
        methods: {
            updateValue: function (event) {
                this.$emit('input', evnet.target.value);
            }
        }
    })

    var app = new Vue({
        el: '#app',
        data: {
            total: 0
        },
        methods: {
            handleReduce: function () {
                this.total--;
            }
        }
    })
</script>

实现这样一个具有双向绑定的v-model组件要满足两个要求:接收一个value属性;在有新的value时触发input事件。

1.3.3 非父子组件通信

在实际业务中,除了父子组件通信外,还有很多非父子组件通信的场景,非父子组件一般有两种,兄弟组件和跨多级组件。

在Vue.js 2.x中,推荐使用一个空的Vue实例作为中央事件总线(bus),也就是一个中介,例如:

<div id="app">
    {{message}}
    <compoment-a v-model="total"></compoment-a>
</div>
<script>
    var bus = new Vue;

    Vue.component('component-a',{
        props: ['value'],
        template: '<button @click="handleEvent"></button>',
        methods: {
            handleEvent: function (event) {
                bus.$emit('on-message', '来自组件component-a的内容');
            }
        }
    })

    var app = new Vue({
        el: '#app',
        data: {
            message: ''
        },
        mounted: function() {
            var _this = this;
            //在实例初始化时,监听来自bus实例的事件
            bus.$on('on-message',function (msg) {
                _this.message = msg;
            });
        }
    })
</script>

首先创建一个名为bus的空Vue实例,里面没有任何内容;然后全局定义了组件component-a;最后创建Vue实例app,在app初始化时,也就是 在生命周期mounted钩子函数里监听了来自bus的事件on-message,而在组件component-a中,点击按钮会通过bus把事件on-message发出去,此时app就会接收到来自bus的事件,进而在回调里完成自己的业务逻辑。

除了中央事件总线bus外,还有两种方法可以实现组件间通信:父链和子组件索引。

父链

在子组件中,使用this.$parent可以直接访问该组件的父实例或组件,父组件也可以通过this.$children访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件。例如:

<div id="app">
    {{message}}
    <compoment-a></compoment-a>
</div>
<script>
    Vue.component('component-a',{
        props: ['value'],
        template: '<button @click="handleEvent">通过父链直接修改数据</button>',
        methods: {
            handleEvent: function (event) {
                //访问到父链后,可以做任何操作,比如直接修改数据
                this.$parent.message = '来自组件component-a的内容';
            }
        }
    });

    var app = new Vue({
        el: '#app',
        data: {
            message: ''
        }
    })
</script>

尽管Vue允许这样操作,但在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样是的父子组件紧耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只看组件自己能修改它的状态。父子组件最好还是通过props和$emit()来通信。

子组件索引

当子组件较多时,通过this.$chilren来一一遍历出我们需要的一个组件实例是比较困难的,尤其是组件动态渲染时,它们的序列是不固定的。Vue提供了子组件索引的方法,用特殊的属性ref来为子组件指定一个索引名称,例如:

<div id="app">
    <button @click="handleRef">通过ref获取子组件实例</button>
    <compoment-a ref="comA"></compoment-a>
</div>
<script>
    Vue.component('component-a',{
        template: '<div>子组件</div>',
        data: function () {
            return {
                message: '子组件内容'
            }
        }
    });

    var app = new Vue({
        el: '#app',
        methods: {
            handleRef: function () {
                //通过$refs来访问指定的实例
                var msg = this.$refs.comA.message;
                console.log(msg);
            }
        }
    })
</script>

在父组件模板中,子组件标签上使用ref指定一个名称,并在父组件内通过this.$refs来访问指定名称的子组件。

$refs只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用$refs。

1.4.1 什么是slot

当需要让组件组合使用,混合父组件的内容和子组件的模板时,就会用到slot,这个过程叫作内容分发(transclusion)。props传递数据、event触发事件和slot内容分发就构成了Vue组件的3个API来源,再复杂的组件也是由这3部分构成的。

1.4.2 作用域

正式介绍slot前,需要先知道一个概念:编译的作用域。比如父组件中有如下模板:

<child-component>
    {{message}}
</child-component>

这里的message就是一个slot,但是它绑定的是父组件的数据,而不是组件<child-component>的数据。

1.4.3 slot的用法

单个slot

在子组件内使用特殊的<slot>元素就可以为这个子组件开启一个slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将代替子组件的<slot>标签及它的内容。例如:

<div id="app">
    <child-component>
        <p>分发的内容</p>
        <p>更多分发的内容</p>
    </child-component>
</div>
<script>
    Vue.component('child-component',{
        template: '\
        <div>\
            <slot>\
              <p>如果父组件没有插入内容,我将作为默认出现</p>\
            </slot>\
        </div>'
    });

    var app = new Vue({
        el: '#app',
    })
</script>

子组件child-component的模板内定义了一个<slot>元素,并且用一个<p>作为默认的内容,在父组件没有使用slot时,会渲染这段默认的文本;如果写入了slot,那就会替换整个<slot>。所以上例渲染后的结果为:

<div id="app">
    <div>
        <p>分发的内容</p>
        <p>更多分发的内容</p>
    </div>
</div>

注意,子组件<slot>内的备用内容,它的作用域是子组件本身。

具名slot

给<slot>元素指定一个name后可以分发多个内容,具名slot可以与单个slot共存,例如:

<div id="app">
    <child-component>
        <h2 slot="header">标题</h2>
        <p>正文内容</p>
        <p>更多的正文内容</p>
        <div slot="footer">底部信息</div>
    </child-component>
</div>
<script>
    Vue.component('child-component',{
        template: '\
        <div class="container">\
            <div class="header">\
                <slot name="header"></slot>\
            </div>\
            <div class="main">\
                <slot></slot>\
            </div>\
            <div class="footer">\
                <slot name="footer"></slot>\
            </div>\
        </div>'
    });

    var app = new Vue({
        el: '#app',
    })
</script>

子组件内声明了3个<slot>元素,其中在<div class="main">内的<slot>没有使用name特性,它将作为默认的slot出现,父组件没有使用slot特性的元素与内容都将出现在这里。

如果没有指定默认的匿名slot,父组件内多余的内容片段都将被抛弃。

上例最终渲染后的结果为:

<div id="app">
    <div class="container">
        <div class="header">
            <h2>标题</h2>
        </div>
        <div class="main">
            <p>正文内容</p>
            <p>更多的正文内容</p>
        </div>
        <div slot="footer">
            <div>底部信息</div>
        </div>
    </child-component>
</div>

1.4.4 作用域插槽

作用域插槽是一种特殊的slot,使用一个可以复用的模板替换已渲染的元素。例如:

<div id="app">
    <child-component>
       <template scope="props">
           <p>来自父组件的内容</p>
           <p>{{props.msg}}</p>
       </template>
    </child-component>
</div>
<script>
    Vue.component('child-component',{
        template: '\
        <div class="container">\
            <slot msg="来自子组件的内容"></slot>\
        </div>'
    });

    var app = new Vue({
        el: '#app',
    })
</script>

观察子组件的模板,在<slot>元素上有个类似props传递数据给组件的写法msg="xxx",将数据传到插槽。父组件中使用了<template>元素,而且有一个scope="props"的特性,这里的props只是一个临时变量,就像v-for="iten in items"里的item一样。template内可以通过临时变量props访问来自子组件插槽的数据msg。

将上面的示例渲染后的最终结果为:

<div id="app">
    <div>
        <div class="container">
            <p>来自父组件的内容</p>
            <p>来自子组件的内容</p>
        </div>
    </div>
</div>

1.4.5 访问slot

Vue.js 2.x提供了用来访问被slot分发的内容的方法$slots,例如:

<div id="app">
    <child-component>
        <h2 slot="header">标题</h2>
        <p>正文内容</p>
        <p>更多的正文内容</p>
        <div slot="footer">底部信息</div>
    </child-component>
</div>
<script>
    Vue.component('child-component',{
        template: '\
        <div class="container">\
            <div class="header">\
                <slot name="header"></slot>\
            </div>\
            <div class="main">\
                <slot></slot>\
            </div>\
            <div class="footer">\
                <slot name="footer"></slot>\
            </div>\
        </div>',
        mounted: function () {
            var header = this.$slots.header;
            var main = this.$slots.default;
            var footer = this.$slots.footer;
            console.log(footer);
            console.log(footer[0].elm.innerHTML);
        }
    });

    var app = new Vue({
        el: '#app',
    })
</script>

通过$slots可以访问某个具名solt,this.$slots.default包括了所有没有被包含在具名solt中的节点。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值