天天看点

Vue之组件详解

组件

简介

  • 地位:组件是Vue.js最核心的功能,也是整个框架设计最精彩的地方
  • 何为组件:没见过的自定义标签就是组件
  • 使用条件:在任何使用Vue的地方都可以直接使用
  • 命名:推荐用小写+减号分隔的形式命名

简单使用

例1:

<div id="app">
			<table>
				<tbody is="my-component"></tbody>
			</table>
		</div>
		<script type="text/javascript" src="../js/vue.js">
		</script>
		<script type="text/javascript">
			var Child = {template:'<div>这是一个组件</div>'};
			// Vue.component('my-component', Child); 全局注册
			var app = new Vue({
				el:'#app',
				components:{ // 局部注册
					'my-component':Child
				}
			})
		</script>
           

这个例子是在table中使用组件,其中涉及到两个需要注意的知识点:

  1. Vue组件的模板在某些情况下会受到HTML的限制,比如

    <table>

    内规定只允许是

    <tr>

    <td>

    <th>

    等表格元素,所以在

    <table>

    中直接使用组件是无效的,这种情况下,就使用我们上述例子中的方法——使用特殊的is属性来挂载组件,最终tbody在渲染的时候,会被替换为组件的内容。常见的限制元素还有

    <ul>

    <ol>

    <select>

  2. template的DOM结构必须被一个元素包含,如果直接写成“这是一个组件”,不带"

    <div></div>

    "是无法渲染的。

例2:

知识点:

在组件中像Vue实例一样使用其他选项(如data、computed、methods等)。但在使用data时,和实例稍有区别,data必须是函数,然后将数据return出去。

<div id="app">
			<table>
				<tbody is="my-component"></tbody>
			</table>
		</div>
		<script type="text/javascript" src="../js/vue.js">
		</script>
		<script type="text/javascript">
			var Child = {template:'<div>{{message}}</div>', data:function(){
					return {
						message:'这是一个组件2'
					}
				}
			};
			// Vue.component('my-component', Child); 全局注册
			var app = new Vue({
				el:'#app',
				components:{ // 局部注册
					'my-component':Child
				}
			})
		</script>
           

使用props传递数据

  • 简介:一种父组件向子组件传递数据或参数的方式。
  • 使用方法:在子组件中通过使用选项

    props

    来声明需要从父级接收的数据,

    props

    的值可以是两种,一种是字符串数组,一种是对象。
  • props与组件data的区别:props来自父级,data中的是组件自己的数据,作用域是组件本身,这两数据都可以在模板template及计算属性computed和方法methods中使用。
  • 例子:
    1. props的值为字符串数组:
      <div id="app">
      			<my-component warning-text="提示信息"></my-component>
      		</div>
      		<script type="text/javascript" src="../js/vue.js">
      		</script>
      		<script type="text/javascript">
      			var Child = {template:'<div>{{warningText}}</div>',props:['warningText']};
      			// Vue.component('my-component', Child); 全局注册
      			var app = new Vue({
      				el:'#app',
      				components:{ // 局部注册
      					'my-component':Child
      				}
      			})
      		</script>
                 
      • 注意:
        1. 由于HTML特性不区分大小写,当使用DOM模板时,驼峰命名(camelCase)的props名称要转为短横分隔命名(kebab-case)。在字符串模板中可以忽略这些限制。
        2. 如果要传递的数据并不是直接写死的,可以使用指令v-bind来动态绑定props的值,当父组件的数据变化时,也会传递给子组件。
        3. 如果要直接传递数字、布尔值、数组、对象,而且不使用v-bind,传递的仅是字符串。
        4. 在Vue2.x通过props传递数据是单向的了,也就是父组件数据变化时会传递给子组件,反过来不行。(这里指的数据是指除对象和数组外的一般数据)
        5. 在业务中遇到两种需要修改prop的情况,一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改(在组件data中声明一个数据,引用父组件的prop)。另一种就是prop作为需要被转变的原始值传入,这种情况使用计算属性就可以了。
        6. 在JavaScript中对象和数组是引用类型,指向同一个内存空间,所以props是对象和数组时,在子组件内改变是会影响父组件的。
    2. props值为对象:
      • 何时用?

        答:当prop需要验证时,就需要对象写法。如果传入数据不符合规则,会在控制台发出警告。

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

使用$emit传递数据

  • 简介:一种子组件向父组件传递数据或参数的方式。
  • 使用方法:子组件通过

    $emit()

    来触发事件,父组件用

    $on()

    来监听子组件的事件或直接在子组件的自定义标签上使用

    v-on

    来监听子组件触发的自定义事件。
  • 解除监听:

    $off()

  • 示例:
    • 描述:子组件有两个按钮,分别实现+1和-1效果,在改变组件的data的

      counter

      后,通过

      $emit()

      再把它传递给父组件,父组件通过

      $on()

      v-on

      来监听子组件触发的事件。
    <div id="app">
    			<p>{{total}}</p>
    			<!-- <my-component @increase="handleGetTotal" @decrease="handleGetTotal"></my-component> -->
    			<my-component ref="child"></my-component>
    		</div>
    		<script type="text/javascript" src="../js/vue.js">
    		</script>
    		<script type="text/javascript">
    			Vue.component('my-component', {
    				template:'\
    					<div>\
    						<button @click="handleIncrease">+1</button>\
    						<button @click="handleDecrease">-1</button>\
    					</div>',
    				data:function(){
    					return {
    						count:0
    					}
    				},
    				methods:{
    					handleIncrease:function(){
    						this.count++;
    						this.$emit('increase', this.count);
    					},
    					handleDecrease:function(){
    						this.count--;
    						this.$emit('decrease', this.count);
    					}
    				}
    			})
    			var app = new Vue({
    				el:'#app',
    				data:{
    					total:0
    				},
    				methods:{
    					handleGetTotal:function(total){
    						this.total = total;
    					}
    				},
    				mounted:function(){
    					this.$refs.child.$on('increase', this.handleGetTotal);
    					// this.$refs.child.$once('increase', this.handleGetTotal); 自定义事件只触发一次
    					this.$refs.child.$on('decrease', this.handleGetTotal);
    				}
    			})
    		</script>
               
  • 补充:除了用

    v-on

    在组件上监听自定义事件外,也可以监听DOM事件,这时可以用

    .native

    修饰符表示监听的是一个原生事件,监听的是该组件的根元素。例:

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

  • 使用v-model:
    • Vue2.x可以在自定义组件上使用v-model指令
    • 在下面示例中,实现的效果仍是点击按钮+1,不过这次组件

      $emit()

      的事件名是特殊的input,在使用组件的父级,并没有在

      <my-component>

      上使用

      @input="handler"

      ,而是直接用

      v-model

      绑定的一个

      数据total

    <div id="app">
    			<p>总数:{{total}}</p>
    			<my-component v-model="total" ></my-component>
    		</div>
    		<script type="text/javascript" src="../js/vue.js">
    		</script>
    		<script type="text/javascript">
    			Vue.component('my-component',{
    				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>
               
    在上例的v-model,相当于下面的操作:
    <div id="app">
    			<p>总数:{{total}}</p>
    			<my-component @input="handleGetTotal" ></my-component>
    		</div>
    		<script type="text/javascript" src="../js/vue.js">
    		</script>
    		<script type="text/javascript">
    			Vue.component('my-component',{
    				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
    				},
    				methods:{
    					handleGetTotal(total){
    						this.total = total;
    					}
    				}
    			})
    		</script>
               
    利用v-model这一特殊用途,还可以创建自定义表单输入组件,进行数据双向绑定,例如:
    <div id="app">
    			<p>总数:{{total}}</p>
    			<my-component v-model="total" ></my-component>
    			<button @click="handleReduce">-1</button>
    		</div>
    		<script type="text/javascript" src="../js/vue.js">
    		</script>
    		<script type="text/javascript">
    			Vue.component('my-component',{
    				props:['value'],
    				template:'<input :value="value" @input="updateValue">',
    				methods:{
    					updateValue:function(event){
    						this.$emit('input', event.target.value);
    					}
    				}
    			});
    			var app = new Vue({
    				el:'#app',
    				data:{
    					total:0
    				},
    				methods:{
    					handleReduce:function(){
    						this.total--;
    					}
    				}
    			})
    		</script>
               
    实现上述一个具有双向绑定的v-model组件要满足如下2个要求:
    1. 接收一个value属性
    2. 在有新的value时触发input事件

使用中央事件总线bus传递数据

  • 简介:一种适用于任意组件间通信的方式
  • 安装:
    new Vue({
    	......
    	beforeCreate() {
    		Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
    	},
        ......
    }) 
               
  • 使用事件总线:
    1. 接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身。
      methods(){
        demo(data){......}
      }
      ......
      mounted() {
        this.$bus.$on('xxxx',this.demo)
      }
                 
    2. 提供数据:

      this.$bus.$emit('xxxx',数据)

  • 解绑时机:在

    beforeDestroy

    钩子中,用

    $off

    去解绑当前当前组件所用到的事件。
  • 示例:
    <div id="app">
    			<component-a></component-a>
    			<br/>
    			<component-b></component-b>
    			<button @click="sendMessage">点我向儿子组件发送数据</button>
    		</div>
    		<script type="text/javascript" src="../js/vue.js">
    		</script>
    		<script type="text/javascript">
    			var app = new Vue({
    				el:'#app',
    				data:{
    					message:'我是根组件的数据'
    				},
    				components:{
    					'component-a':{
    						template:'<div>\
    						这是来自根组件的信息:{{messageRoot}}<br/>\
    						这是来自兄弟组件的信息:{{messageBrother}}\
    						</div>',
    						data:function(){
    							return {
    								messageRoot:'',
    								messageBrother:''
    							}
    						},
    						methods:{
    							getRootMessage:function(message){
    								this.messageRoot = message;
    							},
    							getBrotherMessage:function(message){
    								this.messageBrother = message;
    							}
    						},
    						mounted:function(){
    							// 给bus绑定自定义事件,回调留在自身,说明我想获得数据
    							this.$bus.$on('RootMessage', this.getRootMessage); 
    							this.$bus.$on('BrotherMessage', this.getBrotherMessage);
    						}
    					},
    					'component-b':{
    						template:'<div>\
    							<button @click="sendMessage">点我向兄弟组件发送数据</button>\
    						</div>',
    						data:function(){
    							return {
    								message:'我是兄弟组件的数据'
    							}
    						},
    						methods:{
    							sendMessage(){
    								this.$bus.$emit('BrotherMessage', this.message);
    							}
    						}
    					}
    				},
    				methods:{
    					sendMessage(){
    						this.$bus.$emit('RootMessage', this.message);
    					}
    				},
    				beforeCreate(){
    					Vue.prototype.$bus = this; // 安装中央事件总线bus
    				}
    			})
    		</script>
               

父链

  • 引言:在子组件中,使用

    this.$parent

    可以直接访问该组件的父实例或组件,父组件也可以通过

    this.$children

    访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件。
  • 示例代码:
    <div id="app">
    			<p>这是来自子组件传递的信息:{{message}}</p>
    			<component-a></component-a>
    		</div>
    		<script type="text/javascript" src="../js/vue.js">
    		</script>
    		<script type="text/javascript">
    			Vue.component('component-a',{
    				template:'\
    					<div>\
    						<input type="text" v-model="text" placeholder="请输入需要传递的消息"><br/>\
    						<button @click="handleClick">向父组件发送消息</button>\
    					</div>',
    				methods:{
    					handleClick(){
    						this.$parent.message = this.text;
    					}
    				},
    				data(){
    					return {
    						text:''
    					}
    				}
    			})
    			var app = new Vue({
    				el:"#app",
    				data:{
    					message:''
    				}
    			})
    		</script>
               
  • 补充:尽管Vue允许这样操作,但在业务中,子组件应该尽可能避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只有组件自己能修改它的状态。父子组件最好还是通过

    props

    $emit

    来通信。

子组件索引

  • 引言:当子组件较多时,通过

    this.$children

    来一一遍历出我们需要的一个组件实例是比较困难的,尤其是组件动态渲染时,它们的序列是不固定的。Vue提供了子组件索引的方法,用特殊的属性

    ref

    来为子组件指定一个索引名称。
  • 示例代码:
    • 在父组件模板中,子组件标签上使用ref指定一个名称,并在父组件内通过

      this.$refs

      来访问指定名称的子组件。
    • 注意:

      $refs

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

      $refs

      。以计算属性为例,它是在created之后mounted之前,

      $refs

      是在mounted之后。
    <div id="app">
    			<p>这是来自子组件传递的信息:{{message}}</p>
    			<component-a ref="comA"></component-a>
    			<br/>
    			<button @click="handleClick">打印子组件中文本信息</button>
    		</div>
    		<script type="text/javascript" src="../js/vue.js">
    		</script>
    		<script type="text/javascript">
    			Vue.component('component-a',{
    				template:'\
    					<div>\
    						<input type="text" v-model="text" placeholder="请输入需要传递的消息"><br/>\
    						<button @click="handleClick">向父组件发送消息</button>\
    					</div>',
    				methods:{
    					handleClick(){
    						this.$parent.message = this.text;
    					}
    				},
    				data(){
    					return {
    						text:''
    					}
    				}
    			})
    			var app = new Vue({
    				el:"#app",
    				data:{
    					message:''
    				},
    				methods:{
    					handleClick(){
    						console.log(this.$refs.comA.text);
    					}
    				}
    			})
    		</script>
               
  • 补充:与Vue1.x不同的是,Vue2.x将v-el和v-ref合并为了ref,Vue会自动去判断是普通标签还是组件。可以使用下面代码去查看:
    <div id="app">
    			<p ref="tag">这是来自子组件传递的信息:{{message}}</p>
    			<component-a ref="comA"></component-a>
    			<br/>
    			<button @click="handleClick">点我查看ref特性</button>
    		</div>
    		<script type="text/javascript" src="../js/vue.js">
    		</script>
    		<script type="text/javascript">
    			Vue.component('component-a',{
    				template:'\
    					<div>\
    						<input type="text" v-model="text" placeholder="请输入需要传递的消息"><br/>\
    						<button @click="handleClick">向父组件发送消息</button>\
    					</div>',
    				methods:{
    					handleClick(){
    						this.$parent.message = this.text;
    					}
    				},
    				data(){
    					return {
    						text:''
    					}
    				}
    			})
    			var app = new Vue({
    				el:"#app",
    				data:{
    					message:''
    				},
    				methods:{
    					handleClick(){
    						console.log(this.$refs.comA, this.$refs.tag);
    					}
    				}
    			})
    		</script>
               

使用Slot分发内容

  • 使用场景:当需要让组件组合使用,混合父组件的内容与子组件的模板时,就会用到Slot,这个过程叫作内容分发(transclusion)。
  • 用法:
    • 单个Slot:
      • 在子组件内使用特殊的

        <slot>

        元素就可以为这个子组件开启一个Slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的

        <slot>

        标签及它的内容。示例代码:
        <div id="app">
        			<component-a></component-a>
        			<hr/>
        			<component-a>
        				<ul>
        					<li>苹果</li>
        					<li>梨</li>
        				</ul>
        			</component-a>
        		</div>
        		<script type="text/javascript" src="../js/vue.js">
        		</script>
        		<script type="text/javascript">
        			Vue.component('component-a',{
        				template:'<div>\
        					<slot>\
        						<p>默认内容</p>\
        					</slot>\
        				</div>'
        			})
        			var app = new Vue({
        				el:'#app'
        			})
        		</script>
                   
        • 上述代码解释:子组件componet-a的模板内定义了一个

          <slot>

          元素,并且用一个

          <p>

          作为默认的内容,在父组件没有使用Slot时,会渲染这段默认的文本;如果写入了Slot,那就会替换整个

          <slot>

          。所以上例渲染后结果为:
          <div id="app">
          	<div>
          		<p>默认内容</p>
          	</div>
          	<hr/>
          	<div>
                  <ul>
                      <li>苹果</li>
                      <li>梨</li>
                  </ul>
          	</div>
          </div>
                     
      • 注意:
        • Slot分发的内容,作用域是在父组件上的。
        • 子组件

          <slot>

          内的备用内容,它的作用域是子组件本身。
    • 具名Slot:
      • 定义:指定了name的

        <slot>

        元素称为具名Slot。
      • 补:具名Slot可以与单个Slot共存,用于分发多个内容。
      • 代码示例:
        <div id="app">
        			<component-a>
        				<h2 slot="header">标题</h2>
        				<p>正文内容</p>
        				<p>更多的正文内容</p>
        				<div slot="footer">底部信息</div>
        				<p>正文内容2</p>
                    </component-a>
        		</div>
        		<script type="text/javascript" src="../js/vue.js">
        		</script>
        		<script type="text/javascript">
        			Vue.component('component-a',{
        				template:'<div>\
        					<div class="container">\
        						<div class="header">\
        							<slot name="header">\
        								<p>默认头部内容</p>\
        							</slot>\
        						</div>\
        						<div class="main">\
        							<slot>\
        								<p>默认主体内容</p>\
        							</slot>\
        						</div>\
        						<div class="footer">\
        							<slot name="footer">\
        								<p>默认尾部内容</p>\
        							</slot>\
        						</div>\
        					</div>\
        				</div>'
        			})
        			var app = new Vue({
        				el:'#app'
        			})
        		</script>
                   
      • 上述代码解释:子组件内声明了3个

        <slot>

        元素,其中在

        <div class="main">

        内的

        <slot>

        没有使用name特性,它将作为默认slot出现,父组件没有使用slot特性的元素与内容都将出现在这里。故最终渲染结果为:
        <div id="app">
            <div>
                <div class="container">
                    <div class="header">
                        <h2>
                            标题
                        </h2>
                    </div>
                    <div class="body">
                        <p>
                            正文内容
                        </p>
                        <p>
                            更多的正文内容
                        </p>
                        <p>
                            正文内容2
                        </p>
                    </div>
                   	<div class="footer">
                        <div>
                            底部信息
                        </div>
                    </div>
                </div>
            </div>
        </div>
                   
  • 作用域插槽:
    • 简介:一种特殊的slot,使用一个可以复用的模板替换已渲染元素。
    • 使用场景:既可以复用子组件的slot,又可以使slot内容不一致。
    • 简单示例:
      <div id="app">
      			<component-a>
      				<template scope="props">
      					<p>来自父组件的内容</p>
      					<p>{{props.msg}}</p>
      				</template>
      			</component-a>
      		</div>
      		<script type="text/javascript" src="../js/vue.js">
      		</script>
      		<script type="text/javascript">
      			Vue.component('component-a',{
      				template:'<div class="container">\
      					<slot msg="来自子组件的内容"></slot>\
      				</div>'
      			})
      			var app = new Vue({
      				el:'#app'
      			})
      		</script>
                 
      • 上述代码分析:观察子组件的模板,在

        <slot>

        元素上有一个类似props传递数据给组件的写法msg=“xxxx”,将数据传到了插槽。父组件使用了

        <template>

        元素,而且拥有了一个scope="props"的特性,这里的props知识一个临时变量,就像v-for="item in items"里面的item一样。template内可以通过临时变量 props访问来自子组件插槽的数据msg。上例代码渲染后最终结果为:
        <div>
            <div class="container">
                <p>
                    来自父组件的内容
                </p>
                <p>
                    来自子组件的内容
                </p>
            </div>
        </div>
                   
    • 代表性用例—列表组件:允许组件自定义应该如何渲染列表每一项。
      <div id="app">
      			<my-list :books="books">
      				<!-- 作用域插槽也可以是具名的slot -->
      				<template slot="book" scope="props">
      					<li>{{props.bookName}}</li>
      				</template>
      			</my-list>
      		</div>
      		<script type="text/javascript" src="../js/vue.js">
      		</script>
      		<script type="text/javascript">
      			Vue.component('my-list',{
      				props:{
      					books:{
      						type:Array,
      						default:function(){
      							return [];
      						}
      					}
      				},
      				template:'\
      					<ul>\
      						<slot name="book"\
      							v-for="book in books"\
      							:book-name="book.name">\
      							<!--此处也可以写slot的默认内容-->\
      						</slot>\
      					</ul>\
      				'
      			})
      			var app = new Vue({
      				el:'#app',
      				data:{
      					books:[
      						{name:'《Vue.js实战》'},
      						{name:'《JavaScript语言精粹》'}
      					]
      				}
      			})
      		</script>
                 
      • 上述代码分析:子组件my-list接收一个来自父级的props数组books,并将它在name为book的slot上使用v-for指令循环,同时暴露一个变量bookName。
  • 访问slot:在Vue.js. 1.x中,想要获取某个slot是比较麻烦的,需要用v-el间接获取。而Vue.js 2.x提供了用来访问被slot分发的内容的方法

    $slots

    • 代码示例:
      <div id="app">
      			<component-a>
      				<h2 slot="header">标题</h2>
      				<p>正文内容</p>
      				<p>更多正文内容</p>
      				<div slot="footer">底部信息</div>
      			</component-a>
      		</div>
      		<script type="text/javascript" src="../js/vue.js">
      		</script>
      		<script type="text/javascript">
      			Vue.component('component-a',{
      				template:'\
      					<div class="container">\
      						<div class="header">\
      							<slot name="header"></slot>\
      						</div>\
      						<div class="body">\
      							<slot></slot>\
      						</div>\
      						<div class="footer">\
      							<slot name="footer"></slot>\
      						</div>\
      					</div>\
      				',
      				mounted(){
      					var header = this.$slots.header;// 都是vnode类型数组
      					var main = this.$slots.default;
      					var footer = this.$slots.footer;
      					console.log(header);
      					console.log(footer);
      					console.log(footer[0].elm.innerHTML);
      				}
      			})
      			var app = new Vue({
      				el:'#app'
      			})
      		</script>
                 
    • 示例代码分析:通过

      $slots

      可以访问某个具名slot,

      this.$slots.default

      包括了所有没有被包含在具名slot中的节点。
    • 使用场景:独立组件开发中,业务中几乎用不到。

继续阅读