一、组件的定义
组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以表现为用 is 特性进行了扩展的原生 HTML 元素。所有的 Vue 组件同时也都是 Vue 的实例,所以可接受相同的选项对象 (除了一些根级特有的选项) 并提供相同的生命周期钩子。
二、组件的使用方法
注册组件就是利用Vue.component()
方法,先传入一个自定义组件的名字,然后传入这个组件的配置。
- 全局注册
<div id="app">
<my-component></my-component>
</div>
<script>
Vue.component('my-component', {
template: '<div>我是一个组件</div>'
})
var app = new Vue({
el: '#app',
data: {}
})
</script>
- 全局注册优点:所有的Vue实例都可以用;
- 全局注册缺点:权限太大,容错率降低;
- 局部注册
局部注册在Vue实例下面的
components
选项中
<div id="app">
<app-component></app-component>
</div>
<script>
var app = new Vue({
el: '#app',
data: {},
components: {
'app-component': {
template: '<div>我是app局部注册的一个组件</div>'
}
}
})
</script>
- Vue组件的模板在某些情况下会受到html标签的限制,比如
<table>
中只能还有<tr>
,<td>
这些元素,所以直接在table
中使用组件是无效的,此时可以使用is属性来挂载组件:
<table>
<tbody is="my-component"></tbody>
</table>
三、组件使用的注意事项
-
在
html
中,myMessage
和mymessage
是一致的,,因此在组件中的html
中使用必须使用kebabcase
(短横线)命名方式,在html
中不允许使用驼峰。 -
在组件中, 父组件给子组件传递数据必须用短横线。在
template
中,必须使用驼峰命名方式,若为短横线的命名方式。则会直接保错。 -
在组件的
data
中,用this.XXX
引用时,只能是驼峰命名方式。若为短横线的命名方式,则会报错。 -
template
中的内容必须被一个DOM元素包括 ,也可以嵌套。 -
在组件的定义中,除了template之外的其他选项—
data
,computed
,methods
。 -
data必须是一个方法
<div id="app">
<btn-component></btn-component>
<btn-component></btn-component>
</div>
<script>
var app = new Vue({
el: '#app',
components: {
'btn-component': {
template: '<button @click="count++">{{count}}</button>',
data: function(){
return {
count: 0
}
}
}
}
})
</script>
四、父组件给子组件 使用props传递数据
在组件中使用props来从父亲组件接收参数,注意,在props
中定义的属性,都可以在组件中直接使用
<div id="app">
<child-component msg="我是来自父组件的内容"></child-component>
</div>
<script>
var app = new Vue({
el: '#app',
components: {
'child-component': {
props: ['msg'],
template: '<div>{{msg}}</div>'
}
}
})
</script>
propps
来自父级,而组件中data
return
的数据就是组件自己的数据,两种情况作用域就是组件本身,可以在template
,computed
,methods
中直接使用。
这里有一个问题,在上面的例子中msg
是写死的内容,有的时候传递的数据并不是写死的,而是通过父级动态的数据,这个时候怎么做呢?我们可以通过v-bind
来进行绑定。
<div id="app">
我是内容呀:<input type="text" v-model="parentmsg">
<bind-component :msg="parentmsg"></bind-component>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
parentmsg: '今天的天气不太好'
},
components: {
'bind-component': {
props: ['msg'],
template: '<div>{{msg}}</div>'
}
}
})
</script>
input
v-model
绑定的是父组件中的data
选项中的数据,然后使用v-bind
把绑定的数据传递给子组件,用props
进行接受,并且可以在template
中直接使用,这就是使用v-bind
动态绑定父组件来的内容。
五、单向数据流
- 解释:通过
props
传递数据 是单向的了, 也就是父组件数据变化时会传递给子组件,但是反过来不行。 - 目的:是尽可能将父子组件解稿,避免子组件无意中修改了父组件的状态。
- 应用场景:业务中会经常遇到两种需要改变
prop
的情况。
一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件
data
内再声明一个数据,引用父组件的prop
步骤一:注册组件; 步骤二:将父组件的数据传递进来,并在子组件中用props
接收; 步骤三:将传递进来的数据通过初始值保存起来;
<div id="app">
<my-component msg="我是父组件传递的数据"></my-component>
</div>
<script>
Vue.component('my-component',{
props: ['msg'],
template: '<div>{{count}}</div>',
data: function(){
return {
//props 中的值可以通过 this.xxx 直接来进行获取
count: this.msg
}
}
})
var app = new Vue({
el: '#app',
data: { }
})
</script>
我们为什么不直接用this.msg
,而是将传递进来的数据通过初始值保存起来呢?这样做有一个好处,之后只需要维护count
就可以了,并且count
是在其data
中已经定义的数据。之后不管msg
怎么变化,都会传给count
。
另一种情况就是
prop
作为需要被转变的原始值传入。这种情况用计算属性就可以了 步骤一:注册组件; 步骤二:将父组件的数据传递进来,并在子组件中用props
接收; 步骤三:将传递进来的数据通过计算属性进行重新计算;
<div id="app">
<!-- 通过 input 中输入的数据直接改变 div 的宽度 -->
请输入宽度:<input type="text" v-model="width">
<width-component :width="width"></width-component>
</div>
<script>
Vue.component('width-component', {
props: ['width'],
template: '<div :style="style"></div>',
computed: {
style: function(){
return {
width: this.width + 'px',
height: '100px',
background: 'skyblue'
}
}
}
})
var app = new Vue({
el: '#app',
data: {
width: 0
}
})
</script>
六、数据验证
数据验证主要是对props
传递过来的数据进行类型的验证,并且可以给它指定一些选项,比如说给它设定一个默认值或者说必须设置它的数据类型。验证的 type 类型可以是:
- String
- Number
- Boolean
- Object
- Array
- Function
Vue.component ( 'my-compopent ', {
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;
}
}
}
});
七、组件通信
组件关系可分为父子组件通信、兄弟组件通信、跨级组件通信。
1. 自定义事件—子组件给父组件传递数据
- 使用
v-on
除了监昕 DOM 事件外,还可以用于组件之间的自定义事件。 - JavaScript 的设计模式 一一观察者模式,
dispatchEvent
和addEventListener
这两个方法。 Vue 组件也有与之类似的一套模式,子组件用$emit()
来 触发事件 ,父组件用$on()
来 监昕子组件的事件 。
第一步:自定义事件; 第二步: 在子组件中用
$emit
触发事件,第一个参数是事件名,后边的参数是要传递的数据; 第三步:在自定义事件中用一个参数来接受;
<div id="app">
<h3>你现在卡里余额是:{{total}}元</h3>
<child-component @change="handleTotal"></child-component>
</div>
<script>
Vue.component('child-component',{
template: `
<div>
<button @click="handleincrease">+100</button>
<button @click="handlereduce">-100</button>
</div>
`,
data: function(){
return {
count: 2000
}
},
methods: {
handleincrease: function(){
this.count = this.count + 100
this.$emit('change', this.count)
},
handlereduce: function(){
this.count = this.count - 100
this.$emit('change', this.count)
}
}
})
var app = new Vue({
el: '#app',
data: {
total: 2000
},
methods: {
handleTotal: function(value){
//此处的形参 value 就是传递过来的数据
this.total = value
}
}
})
</script>
在这里自定义组件中,定义了两个button
,一个加100,一个减100,点击加减按钮的时候都会触发一个事件,加100的时候把它组件中定义的 count
加100,减100的时候就是减100。问题的关键是要把this.count
传递给父组件,那怎么传呢,可以在子组件的标签上自定义一个change
事件。自定义事件后,需要在方法中,点击按钮的时候同时触发这个change
事件。$emit()
第一个参数是触发的事件名change
,第二个参数是要传递的数据this.count
。这样触发change
事件后就会执行handleTotal
,它有一个参数value
就是传递过来的数据,把value
赋值给this.total
,页面就会重新渲染。
2. 在组件中使用v-mode
在上一个例子中我们首先是渲染模板,模板渲染完之后,然后$emit()
这个change
事件,触发事件后执行``handleTotal`,用一个参数来接收。这里还是从例子入手,在上面例子的代码中,现在就留下一个加100的按钮,这样比较简单,可以更好的理解。
<div id="app">
<h3>你现在卡里余额是:{{total}}元</h3>
<child-component v-model="total"></child-component>
</div>
<script>
Vue.component('child-component',{
template: `
<div>
<button @click="handleincrease">+100</button>
</div>
`,
data: function(){
return {
count: 2000
}
},
methods: {
handleincrease: function(){
this.count = this.count + 100
this.$emit('input', this.count)
}
}
})
var app = new Vue({
el: '#app',
data: {
total: 2000
}
})
</script>
**v-model
**其实就是绑定了input
事件,当触发input
的时候,input
事件就会自动接收传递过来的参数,并赋值给已绑定的total
$emit
的代码,这行代码实际上会触发一个 input
事件, input
后的参数就是传递给v-model
绑定的属性的值。v-model
其实是一个语法糖,这背后其实做了两个操作:
v-bind
绑定一个value
属性v-on
指令给当前元素绑定input
事件 要使用v-model
,要做到:- 接收一个
value
属性 - 在有新的
value
时触发input
事件
3. 非父组件之间的通信
首先我们先看下官网的描述:
我们来画一个草图,在根组件中有一个子组件A和子组件B,A和B不是父子组件关系。那A要向B传递数据改怎么做呢?
官网给我们提供的做法是这样的,首先创建一个空的Vue
实例,里面是没有内容的。然后在组件A中写一个方法用来触发这个事件,在组件B中使用钩子函数在实例创建的时候去监听这个事件。
bus
的重要相当于一个"中介",A触发了事件,bus
就知道了就提醒B,B就直接去执行它监听的方法。
通过一个例子来加深下理解:
<div id="app">
<a-component></a-component>
<b-component></b-component>
</div>
<script>
Vue.component('a-component', {
template: `
<div>
<button @click="handle">点击我想B组件传递数据</button>
</div>
`,
data: function(){
return {
msg: '我是来自A组件的内容'
}
},
methods: {
handle: function(){
this.$root.bus.$emit('pass', this.msg)
}
}
})
Vue.component('b-component', {
template: '<div></div>',
created: function(){
////A组件在实例创建的时候就监听事件-pass事件
this.$root.bus.$on('pass',function(value){
alert(value)
})
}
})
var app = new Vue({
el: '#app',
data: {
bus: new Vue()
}
})
</script>
要想从A组件向B组件传递数据,首先要在根组件定义一个bus
中介,然后在A组件点击按钮的时候就要传递数据,那就执行一个方法。this.$root
拿到这个bus
中介,bus
中介去触发这个pass
事件,并且把this.msg
做为参数传递过去。
在B组件中Vue实例创建的时候,就去监听pass
事件,然后在后面的function
中接收一个参数。
3.1. 父链:this.$parent
子组件有时候需要访问父组件中的内容,那该怎么访问呢?
<div id="app">
<child-component></child-component>
<div>{{msg}}</div>
</div>
<script>
Vue.component('child-component', {
template: '<button @click="setFatherData">通过点击我修改父亲的数据</button>',
methods: {
setFatherData: function(){
this.$parent.msg = '数据已经被修改了'
}
}
})
var app = new Vue({
el: '#app',
data: {
msg: '数据还没有被修改'
}
})
</script>
首先点击子组件的按钮执行了setFatherData
方法,this.$parent.msg
直接拿到了父组件中的msg
,就可以对它进行修改。
3.2. 子链:this.$refs
父链可以通过this.$parent
拿到,子链该怎么拿呢?子链就是this.$children
。但是有一个问题,子组件可能会非常多,如果直接用this.$children
的话,它就会拿到全部的。我们如果采用一一遍历的方法,肯定也是不太现实的。所以Vue.js给我们提供了一种子组件中使用的索引,也就是ref属性,来为子组件指定一个属性名称。
<div id="app">
<a-component ref="A"></a-component>
<b-component ref="B"></b-component>
<button @click='getChildData'>我是父组件的按钮,我要拿到组件的内容</button>
<div>{{formchild}}</div>
</div>
<script>
Vue.component('a-component', {
template: '<div></div>',
data: function(){
return {
msg: '我是来自A组件的内容'
}
}
})
Vue.component('b-component', {
template: '<div></div>',
data: function(){
return {
msg: '我是来自B组件的内容'
}
}
})
var app = new Vue({
el: '#app',
data: {
formchild: '数据还未拿到'
},
methods: {
getChildData: function(){
//$refs 用来拿子组件中的数据
this.formchild = this.$refs.B.msg
}
}
})
</script>
这个例子中,通过一个ref
属性,就可以给每个组件加索引。然后父组件如果想拿子组件中的内容,直接用this.$refs.ref属性的值.data中的属性
八、使用slot分发内容
1. 什么是slot(插槽)
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为 内容分发.Vue.js 实现了一个内容分发 API,使用特殊的 ‘slot’ 元素作为原始内容的插槽。
2. 编译的作用域
在深入内容分发 API 之前,我们先明确内容在哪个作用域里编译。假定模板为:
<child-component>
{{ message }}
</child-component>
message
应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件。组件作用域简单地说是:
- 父组件模板的内容在父组件作用域内编译;
- 子组件模板的内容在子组件作用域内编译。
3. 插槽的用法
父组件的内容与子组件相混合,从而弥补了视图的不足 混合父组件的内容与子组件自己的模板
3.1. 单个插槽:
<div id="app">
<my-component>
<p>我是父组件中的内容</p>
</my-component>
</div>
<script>
Vue.component('my-component',{
template: `
<div>
<slot>如果父组件没有插入内容,我就作为默认出现</slot>
</div>
`
})
var app = new Vue({
el: '#app',
data: { }
})
</script>
slot
相当于子组件设置了一个地方,如果在调用它的时候,往它的开闭标签之间放了东西,那么它就把这些东西放到slot
中。
-
当子组件中没有
slot
时,父组件放在子组件标签内的东西将被丢弃; -
子组件的
slot
标签内可以放置内容,当父组件没有放置内容在子组件标签内时,slot
中的内容会渲染出来; -
当父组件在子组件标签内放置了内容时,
slot
中的内容被丢弃
3.2. 具名插槽:
slot
可以有很多个,那么子组件对于父组件放置的多余的内容如何放到各个slot
中呢?方法就是子组件给每个slot
起一个名字name
,父组件放置多余的元素时,给每个元素的slot属性分配一个代表slot
的名字。到时候,多余的内容就会根据自己的slot属性去找具有对应名字的slot
元素。
<div id="app">
<my-component>
<div slot="header">我会标题标题标题</div>
<p>我是正文内容</p>
<p>我是第二段正文内容</p>
<div slot="footer">我是底部信息</div>
</my-component>
</div>
<script>
Vue.component('my-component',{
template: `
<div>
<div class="header">
<slot name="header"></slot>
</div>
<div class="content">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
`
})
var app = new Vue({
el: '#app',
data: { }
})
</script>
这就是具名插槽的用法,在组合使用组件的时候内容分发特别重要。为什么需要具名插槽呢?在子组件中把页面的结构都定好了之后,只需要传递内容就可以了。比如说今天传递的是“我是标题”,明天又想传递别的。那只需要把数据传递进去,再指定一个slot
的名字就可以。我们只需要关心它的数据,视图已经在里面了,因此这就是视图不变数据变。
4. 作用域插槽
作用域插槽是一种特殊的slot
,使用一个可以复用的模板来替换已经渲染的元素。
- 从子组件获取数据。
- template模板是不会被渲染的。
<div id="app">
<my-component>
<template slot="abc" slot-scope="prop">
{{prop.text}}
</template>
</my-component>
</div>
<script>
Vue.component('my-component',{
template: `
<div>
<slot text="我是来自子组件的数据" name="abc"></slot>
</div>
`
})
var app = new Vue({
el: '#app',
data: { }
})
</script>
首先在子组件中定义一个slot
,slot
里面写要传递的数据。在template
模板中定义一个slot-scope
,是自己定义的临时的变量名。然后在这个模板中用文本插值的形式,用自己定义的变量名.
要拿到的属性名。
5. 访问slot
单个插槽、具名插槽和作用域插槽我们在应用过程中,可能会用到去访问这个slot
,也就是拿到slot
中的一些数据,那怎么访问呢?
通过this.$slots.(NAME)
//结合上面具名插槽的代码,在 Vue 实例挂载结束的时候执行 mounted
mounted:function () {
//访问插槽
var header = this.$slots.header;
var text = header[0].elm.innerText;
var html = header[0].elm.innerHTML;
console.log(header)
console.log(text)
console.log(html)
}
this.$slots.header
拿到的是一个虚拟节点,这个虚拟节点中只有一个元素,所以要用0
去访问里面的所有元素。header[0]
拿到elm
,然后.elm
下面有innerHTML
和innerText
。
九、组件高级用法–动态组件
Vue给我们提供 了一个元素叫component
- 作用是: 用来动态的挂载不同的组件
- 实现:使用is特性来进行实现的
<!-- 需求:通过点击不同的按钮切换不同的视图 -->
<div id="app">
<component :is="thisView"></component>
<button @click="handleView('A')">第1句</button>
<button @click="handleView('B')">第2句</button>
<button @click="handleView('C')">第3句</button>
<button @click="handleView('D')">第4句</button>
</div>
<script>
Vue.component('compA',{
template: '<div>离离原上草</div>'
})
Vue.component('compB',{
template: '<div>一岁一枯荣</div>'
})
Vue.component('compC',{
template: '<div>野火烧不尽</div>'
})
Vue.component('compD',{
template: '<div>春风吹又生</div>'
})
var app = new Vue({
el: '#app',
data: {
thisView: 'compA'
},
methods: {
handleView: function(tag){
this.thisView = 'comp' + tag
}
}
})
</script>