- 模板:(template)模板声明了数据和最终展现给用户的DOM之间的映射关系。
- 初始数据:(data)一个组件的初始数据状态。对于可复用的组件来说,通常是私有的状态。
- 接收外部参数:(props)组件之间通过参数来进行数据的传递和共享。参数默认是单向绑定(由上至下),但也可以显式声明为双向绑定。
- 方法:(methods)对数据的改动操作一般都在组件的方法内进行。可以通过v-on指令将用户输入事件和组件方法进行绑定。
- 声明周期钩子函数:(lifecycle hooks)一个组件会触发多个生命周期钩子函数,比如created、attached、destroyed等。在这些钩子函数中,我们可以封装一些自定义的逻辑。和传统的MVC相比,这可以理解为Controller的逻辑被分散到了这些钩子函数中。
一、注册组件
- 全局组件
- 局部组件
1.1全局组件注册:Vue.component('didi-component',DIDIComponent)
参数1('didi-component'):注册组件的名称,即在HTML中可以使用对应名称的自定义标签来添加组件:<didi-component></didi-component>,名称除了使用中划线与html中添加自定义标签一致以外,还可以使用小驼峰命名方式来定义名称,同样vue内部会自动匹配到中划线的html自定义标签上,即‘didi-component’同等于‘didiComponent’,也有不规范的写法直接自定义任意英文字符,不采用连接符(中划线)也不采用小驼峰命名,也是可以的,后面示例中会有出现。
参数2(DIDIComponent):注册组件的钩构造函数Function,也可以是Object。
1 //组件构造器构造组件: 2 var MyComponent = Vue.extent({ 3 //选项... 4 }) 5 //传入选项对象注册全局组件: 6 Vue.component('didi-component',{ 7 template:`<div>A custom component!</div>` 8 })
实例代码(代码折叠):
1 <div id="example"> 2 <didi-component></didi-component> 3 </div> 4 <script> 5 var DIDIComponent = Vue.extend({ 6 template:`<div>A custom component!</div>` 7 }) 8 //注册 9 Vue.component('didi-component',DIDIComponent) 10 //创建vue根实例 11 new Vue({ 12 el:'#example' 13 }) 14 </script>
1.2局部组件注册:也可以说是在vue实例上注册组件。
1 <div id="example"> 2 <didi-component></didi-component> 3 </div> 4 <script> 5 //注册局部组件 6 var DIDIComponent = Vue.extend({ 7 template:`<div>i am child!</div>` 8 }); 9 new Vue({ 10 el:'#example', 11 components:{ 12 didiComponent:DIDIComponent 13 } 14 }); 15 </script>
所谓局部组件就是不使用Vue.component()注册组件,而是直接在vue实例上,通过component添加组件,上面的示例中将组件构造写在实例外面,也可以直接写在实例中:
1 new Vue({ 2 el:'#example', 3 components:{ 4 didiComponent:Vue.extend({ 5 template:`<div>i am child!</div>` 6 }) 7 } 8 });
注:现在的vue中构造组件可以不用写Vue.extend()方法,而是可以直接写成对象形式,后面的示例中将全部省略Vue.extend()。
全局组件与局部组件除了全局组件通过Vue.component()方法注册,而局部组件直接通过component添加到vue实例对象上以外。全局组件注册会始终以构造的形式被缓存,而局部组件不被使用时,不会被构造缓存,而是在vue实例需要时才被构造。虽然说将组件构造缓存在内存中可以提高代码执行效率,但是另一方面是消耗大量的内存资源。
除了上面的单个构造注册,也可以直接在模板中使用引用其他局部组件:
1 <div id="example"> 2 <didi-component></didi-component> 3 </div> 4 <script> 5 var Child = { 6 template:`<div>i am child!</div>`, 7 replace:false 8 } 9 var Parent = { 10 template:`<div>//这个包装元素现在不能使用template标签了,以前可以 11 <p>i am parent</p> 12 <br/> 13 <child-component></child-component> 14 </div>`, 15 components:{ 16 'childComponent':Child//在局部组件模板中直接引用其他局部组件 17 } 18 } 19 new Vue({ 20 el:'#example', 21 components:{ 22 didiComponent:Parent 23 } 24 }) 25 </script>
二、数据传递
- props
- 组件通信
- slot
2.1通过props实现数据传输:
通过v-bind在组件自定义标签上添加HTML特性建立数据传递通道;
在组件字段props上绑定自定义标签上传递过来的数据;
1 <div id="example"> 2 <!--将vue实例中的数据resultsList绑定到自定义特性list上--> 3 <didi-component :list="resultsList"></didi-component> 4 </div> 5 <script> 6 //绑定数据 7 var Child = { 8 props:['list'],//通过自定义特性list获取父组件上的resultsList 9 template: `<div> 10 <p> 11 <span>姓名</span> 12 <span>语文</span> 13 <span>数学</span> 14 <span>英语</span> 15 </p> 16 <ul> 17 <li v-for="item in list" :key="item.name"> 18 <span>{{item.name}}</span> 19 <span>{{item.results.language}}</span> 20 <span>{{item.results.math}}</span> 21 <span>{{item.results.english}}</span> 22 </li> 23 </ul> 24 </div>` 25 } 26 var vm = new Vue({ 27 el:'#example', 28 components:{ 29 didiComponent:Child 30 }, 31 data:{ 32 resultsList:[ 33 { 34 name:"张三", 35 results:{language:89,math:95,english:90} 36 }, 37 { 38 name:"李四", 39 results:{language:92,math:76,english:80} 40 }, 41 { 42 name:"王五", 43 results:{language:72,math:86,english:98} 44 } 45 ] 46 } 47 }); 48 </script>
通过上面的示例可以了解到子组件获取父组件上的数据过程,这你一定会有一个疑问,props这里这有什么作用,为什么需要props这个字段?
a.每个组件都有自己独立的实例模型来管理自己身的属性,通过props获取到父组件中自身需要的数据,并使用自身属性缓存数据引用可以提高程序执行效率,如果将父组件上的数据全盘接收过来,对于子组件自身来说会有大量多余的数据,反而降低程序执行效率。
b.通过props数据校验,保证获取到正确无误的数据,提高程序的可靠性。
2.2props接收数据的方式及校验处理:
1 //采用数组形式接收父组件通过自定义特性传递过来的数据 2 props:['data1','data2','data3'...]//数组接收方式只负责数据接收,并不对数据校验处理 3 4 //采用对象形式接收父组件通过自定义特性传递过来的数据 5 props:{ 6 data1:{ 7 type:Array,//校验接收数据类型为Array,type的值还可以是数组,添加数据可符合多种数据类型的校验 8 default:[{name:"我是默认参数",...}],//当data1没有接收到数据时,组件就会使用默认数据渲染 9 required:true,//校验必须接收到数据,不然即使在有默认数据的情况下也会在控制台打印出报错提示 10 validator(value){//校验数据具体信息,参数value接收到的是传递过来的数据,如果方法返回false则表示数据不符合条件,控制台打印出报错提示 11 return value.length < 1; 12 } 13 }
采用数据校验只能保证在数据有误时在控制台打印出相对准确的错误提示,并不会阻止数据渲染,也不会阻塞后面的代码执行。
2.3栈内存传值与单向数据流
1 <div id="example"> 2 父级对象传值:<input type="text" v-model="info.name"/> 3 父级对象属性传值:<input type="text" v-model="obj.name"> 4 父级字符串传值:<input type="text" v-model="name"> 5 <child v-bind:msg1.sync="info" v-bind:msg2="obj.name" v-bind:msg3="name"></child> 6 </div> 7 <script> 8 new Vue({ 9 el:'#example', 10 data:{ 11 info:{ 12 name:'顺风车' 13 }, 14 obj:{ 15 name:"专车" 16 }, 17 name:"快车" 18 }, 19 components:{ 20 'child':{ 21 props:['msg1','msg2','msg3'], 22 template:` <div> 23 接收对象传值:<input type="text" v-model="msg1.name"/> 24 接收对象属性传值:<input type="text" v-model="msg2"> 25 接收字符串传值:<input type="text" v-model="msg3"> 26 </div>` 27 } 28 } 29 }) 30 </script>
示例效果:
通过示例可以看到vue父子组件传值,采用的是父级向子级的单向数据流,当父级数据发生变化时,子级数据会跟着变化。但同时又因为传递值的方式采用的是栈内存赋值方式,如果父级传递给子级的是引用值类型数据,在子级中改变数据也会引发父级数据的更改。
注:在权威指南中有说可以通过给数据传递添加修饰符once和sync来控制数据的但双向绑定,在2.x中测试无效,但添加这两个修饰符不报错。关于个问题我想应该是版本更新优化的,但不能确定,毕竟我没有使用过之前的版本,也没有阅读之前版本的手册和源码,这个一点暂时保留意见。
由于vue父子组件传值采用了栈内存传值,就没有办法保证数据的单向传递,解决这个问题的办法很简单,在子组件中通过克隆该数据用自身的数据引用保存下来就OK了,但要注意在子组件中声明data需要使用function类型。
注:即便传递的数据是原始值类型的数据,也不要直接使用接收的数据,虽然程序能正常运行,但是vue在控制台对直接使用原始值的行为报警告,所以正确的数据接收方式是通过props接收后在子组件自身的数据上再创建一个数据副本来使用。
data(){ return{ //自身数据引用名称:接收父级传递来的引用数据的克隆数据 } }
1 <div id="example"> 2 父级对象传值:<input type="text" v-model="info.name"/> 3 父级对象属性传值:<input type="text" v-model="obj.name"/> 4 父级字符串传值:<input type="text" v-model="name"/> 5 <chiled-component v-bind:msg1="info" v-bind:msg2="obj.name" v-bind:msg3="name"></chiled-component> 6 </div> 7 <script> 8 new Vue({ 9 el:"#example", 10 data:{ 11 info:{ 12 name:"顺风车" 13 }, 14 obj:{ 15 name:"专车" 16 }, 17 name:"快车" 18 }, 19 components:{ 20 chiledComponent:Vue.extend({ 21 props:["msg1","msg2","msg3"], 22 data(){ 23 return { 24 info:{name:this.msg1.name}, 25 obj:{name:this.msg2}, 26 name:this.msg3 27 } 28 }, 29 template:`<div> 30 接收对象传值:<input type="text" v-model="info.name"/> 31 接收对象属性传值:<input type="text" v-model="obj.name"/> 32 接收字符串传值:<input type="text" v-model="name"/> 33 </div>` 34 }) 35 } 36 }) 37 </script>
测试改良后的代码会发现子组件上的info数据发生变化,不再会触发父级数据的渲染,这有效的隔离了子组件对数据的父级数据的影响,更凸显了面向对象的思想,让行为粒度更小,这是好事情,不信你可以考虑一种情况,如果这个模块从最顶层到最底层的组件有很多个,而且还与别的模块共享数据,如果一个组件发生变化就触发所有数据渲染,这是一个多么糟糕的性能问题甚至还会产生很多关联的问题,但可能有的人会说如果本来就需要数据状态共享,即便是这样也不应该采用这种暴力的方法,而是通过切换每一个视图来按需触发数据状态共享。
三、组件通信
在上一节介绍了父子组件数据传递,我们知道vue采用的是单向数据流,这就意味着当子组件需要向父组件传值时就得需要其他手段,毕竟在更多时候子组件很可能会有复用性情况,所以子组件也不能直接去更改父组件的数据,但总会有很多需求需要我们将子组件的计算结果传递给父组件。
Vue通过在父级作用域上定义事件,然后再由子组件使用$emit来实现事件派送,当派送到对应事件作用域时,使用派送过来的参数(数据)触发事件回调函数,这就是组件通信。通过组件通信可以解决子组件向父组件传值,示例:
1 <div id="example"> 2 <!-- 通过transmitdata接收来自子组件的传输指令,然后触发父组件的addComData方法 --> 3 <comment-complate @transmitdata="addComData"></comment-complate> 4 <ul> 5 <li v-for="item in comList" :key="item.id"> 6 <div> 7 <span class="userName">{{item.userName}}</span> 8 <span class="comDate">{{item.date}}</span> 9 </div> 10 <div>{{item.val}}<div> 11 </li> 12 </ul> 13 </div> 14 <script> 15 var comment = { 16 template: `<div> 17 <textarea v-model="comVal"></textarea> 18 <p><button @click="submitData">提交</button></p> 19 </div>`, 20 data(){ 21 return { 22 userId:1001, 23 userName:"他乡踏雪", 24 comVal:"" 25 } 26 }, 27 methods:{ 28 submitData(){ 29 console.log(this.comVal + 'a'); 30 var comDate = new Date(); 31 let comData = { 32 id:Number(''+this.userId + comDate.getTime()), 33 userName:this.userName, 34 date:comDate.toLocaleDateString() + ' ' + comDate.toLocaleTimeString(), 35 val:this.comVal 36 } 37 this.$emit('transmitdata',comData);//通过$emit监听提交留言的点击事件,被触发后将数据传递给自定义方法transmitdata 38 } 39 } 40 } 41 var vm = new Vue({ 42 el:'#example', 43 components:{ 44 commentComplate:comment 45 }, 46 data:{ 47 comList:[ 48 { 49 userName:"南都谷主", 50 id:1001, 51 date:'2019/7/8 上午00:32:55', 52 val:'2017年,在长春园东南隅的如园遗址,工作人员在进行' 53 } 54 ] 55 }, 56 methods:{ 57 addComData(data){ 58 //transmitdata自定义事件接收到数据传递请求后,将传递过来的数据交给addComData处理 59 this.comList.unshift(data); 60 } 61 } 62 }); 63 </script>
如果用来接收数据传输数据的事件是一个原生的DOM事件,就不必使用$emit()来监听事件触发,只需要在原生的事件声明后添加一个‘.navite’后缀就可以自动实现监听事件触发,原生事件本身就是由浏览器自身监听,所以不必要多余的操作。如:@click.navite="父组件的事件监听方法"。但是要注意的是这种事件不具备组件通信能力,因为实质上这个方法是父级组件定义在内部DOM上的一个原生事件,不能被子组件的$emit侦听到,也就是说在子组件内不是无法触发这个事件的。
<comment-complate @click="addComData"></comment-complate>
四、插槽与动态组件
4.1插槽:<slot></slot>
有时候需要在父级组件中给子组件添加一些节点内容,这时候就可以在使用slot来实现,并且还可以在子组件中使用具名插槽,来实现指定位置插入节点。示例:
1 <div id="app"> 2 <child> 3 <span>{{titleName}}:</span><!--插入插槽--> 4 </child> 5 </div> 6 <script> 7 var vm = new Vue({ 8 el:'#app', 9 components:{ 10 child:{ 11 template:` <div> 12 <slot></slot><!--定义插槽--> 13 <input type="text" /> 14 </div>` 15 } 16 }, 17 data:{ 18 titleName:'邮箱' 19 } 20 }); 21 </script>
通过上面的插槽功能就可以动态的切换输入框的标题,在一些需求中有多种内容输入方式,就不需要去定义多个组件来实现,只需要在父级组件来切换输入标题,子组件只负责输入操作,就可以实现在同一个组件上实现多个输入场景。
有了多种内容输入场景就必然需要多种输入提示,这种输入提示必定是需要与输入标题相配合,数据必然是同样与标题内容处于父级组件上,就需要多个插槽来实现,这时候就需要用到具名插槽:
1 <div id="app"> 2 <child> 3 <span slot="title">{{titleName}}:</span><!--插入标题插槽--> 4 <span slot="hint">{{hint}}</span> 5 </child> 6 </div> 7 <script> 8 var vm = new Vue({ 9 el:'#app', 10 components:{ 11 child:{ 12 template:` <div> 13 <slot name="title"></slot><!--定义标签具名插槽--> 14 <input type="text" /> 15 <slot name="hint"></slot><!--定义提示具名插槽--> 16 </div>` 17 } 18 }, 19 data:{ 20 titleName:'邮箱', 21 hint:"请输入正确的邮箱地址" 22 } 23 }); 24 </script>
通过具名插槽可以将父级插入的节点插入到指定的地方,当然这时候你会说可以直接在子组件上来实现这些数据切换,这是肯定可行的,但是使用子组件实现就必然会涉及到父子组件传值。有可能你也会想到这样的功能也可以使用一个父组件就可以实现,为什么还要使用子组件呢?这也当然是可以的,但是这同样涉及到了另一个问题,如果是复杂一点的需求呢?所以,没有绝对的设计优势,这要看具体需求,如果是在比较复杂的需求中就可以通过插槽的方式将输入操作与业务逻辑通过组件层级分离。
插槽除了上面的应用,在接下来的动态组件中也会有很大作用。
4.2动态组件:
所谓动态组件就是在父组件中定义一个子组件,可以通过数据来切换实现引入不同的子组件,这时你一定会想这不就可以通过v-if和v-else来实现吗?注意,v-if和v-else来配合实现只能实现两个组件切换,而且需要在父组件中引入两个子组件,这与动态组件只需要引入一个子组件,并且可以切换多个子组件的功能来比较相差甚远。
语法:<component is=“绑定指定的子组件”></component>
示例:
1 <div id="app"> 2 <button @click="chanegCmp">切换组件</button> 3 <component :is="cmpType"></component> 4 </div> 5 <script> 6 const cmpOne = { 7 template:` <div> 8 <span>组件1:</span> 9 <input type="text" /> 10 </div>` 11 } 12 const cmpTwo = { 13 template:` <div> 14 <span>组件2:</span> 15 <input type="text" /> 16 </div>` 17 } 18 const cmpThree = { 19 template:` <div> 20 <span>组件3:</span> 21 <input type="text" /> 22 </div>` 23 } 24 var vm = new Vue({ 25 el:'#app', 26 data:{ 27 cmpList:["cmpOne","cmpTwo","cmpThree"], 28 cmpType:"cmpOne", 29 cmpPresent:0 30 }, 31 components:{ 32 cmpOne:cmpOne, 33 cmpTwo:cmpTwo, 34 cmpThree:cmpThree 35 }, 36 methods:{ 37 chanegCmp(){ 38 console.log(this.cmpPresent + ' - ' + this.cmpList.length) 39 if( (this.cmpPresent + 1) === this.cmpList.length){ 40 this.cmpPresent = 0; 41 this.cmpType = this.cmpList[this.cmpPresent]; 42 }else{ 43 this.cmpPresent ++; 44 this.cmpType = this.cmpList[this.cmpPresent]; 45 } 46 } 47 } 48 }); 49 </script>
示例实现效果:
虽然可以通过动态组件切换组件,但是上面的示例可能还并不能是我们想要的,因为在切换组件时并不能保留组件的状态,比如我们在第一次切换组件时,输入文本后第二次切换回到这个组件时,输入的文本并不能不被保留,也就是说重新切换回来的组件是一个全新的渲染组件,并不上次的原组件,
针对这种需求vue给我提供了一个标签<keep-alive>,使用这个标签包裹component标签就可以实现保留子组件的状态了,所以示例中的代码可以这样修改:
1 <keep-alive> 2 <component :is="cmpType"></component> 3 </keep-alive>
4.3作用域插槽
在前面的4.1中介绍了插槽,它可以在父级来定义相对灵活的子组件,在前面的示例中只是介绍了父级在子级指定为节点位置插入一些元素节点,这时我想应该又引发了我们的另一个思考,既然插槽可以在父级作用定义子级特定位置的节点,那可不可以实现父级作用域定义子级特定的数据模板呢?也就是子级不仅可以提供一个占位,还可以提供数据引用来实现在父级组件中定义元素的节点和数据渲染。这个光是用文字来描述会有点绕,先来看一个小示例:
1 <div id="app"> 2 <child :userdata="user"><!--获取父组件的数据user,并定义新的引用名称userdata--> 3 <template slot-scope="list"><!--数据模板上通过slot-scope特性接收插槽传来的数据,并定义索引名list--> 4 <span>{{list.username}} -- {{list.userCom}}</span><!--通过模板接收到的数据索引取出数据并渲染到页面--> 5 </template> 6 </child> 7 </div> 8 <script> 9 const child = { 10 props:["userdata"],//接收父组件的数据userdata, 11 data(){ 12 return{ 13 comData:"child" 14 } 15 }, 16 template:` <div> 17 <!--通过插槽将接收到父组件的数据和子组件自身的数据传给模板--> 18 <!--传递数据的方式就是将数据以特性的方式传入slot标签--> 19 <slot 20 :username="userdata.name" 21 :userCom="comData"></slot> 22 <input type="text"/> 23 </div>` 24 } 25 var vm = new Vue({ 26 el:'#app', 27 data:{ 28 user:{ 29 name:"他乡踏雪", 30 } 31 }, 32 components:{ 33 child:child 34 } 35 }); 36 </script>
这里的一个关键就是通过<slot>插槽标签的特性键数据传出,然后通过模板特性slot-scope接收这些数据,再来提供一个示例来进一步了解作用域插槽:
1 <div id="app"> 2 <cmp-two :list="list"> 3 <template slot-scope="list"> 4 <li>{{list.item}} - {{list.index}}</li> 5 </template> 6 </cmp-two> 7 <cmp-two :list="list"> 8 <template slot-scope="list"> 9 <li>{{list.index}} - {{list.item}}</li> 10 </template> 11 </cmp-two> 12 </div> 13 <script> 14 const cmpTwo = { 15 props:['list'], 16 template:`<div> 17 组件2:<input tyle="text"> 18 <ul> 19 <slot v-for="(item,index) in list" 20 :item="item" 21 :index="index"> 22 </slot> 23 </ul> 24 </div>` 25 } 26 var vm = new Vue({ 27 el:'#app', 28 components:{ 29 cmpTwo 30 }, 31 data:{ 32 list:[1,2,3,4,5] 33 } 34 }); 35 </script>