上一部分我们已经学习了通过props从父组件向子组件传递数据,但Vue组件通信的场景有很多种:
很多初学者可能有和我一样的问题,根本分不清什么是父组件,什么是子组件,这让后面的学习非常费劲,所以我们来讲一下怎样分别两者。
一、什么是子组件?什么是父组件?
我们先看一段代码:
<div id="app">
<father-component message="来自父组件的数据"></father-component>
<father-component message="现在显示的是father-component父组件的子组件"></father-component>
<!--数据message就是通过props从父级传递过来的,在组件的自定义标签上直接写该props的名称-->
</div>
<script>
Vue.component('father-component',{
props:['message'],
template:'<div>{{message}}</div>'
});
var app = new Vue({
el:'#app'
})
</script>
这段代码中全局注册、被命名为father-component的组件就是父组件,如果父组件在页面中被使用,那么被使用的自定义标签就是子组件。上例也算是温习了上节课的内容,props可以通过父级组建传递数据到子组件。
分清了什么是子组件和父组件,我们来讲一下v-on指令和自定义事件。
二、自定义事件
当子组件需要向父组件传递数据的时候,需要用到自定义事件。而v-on指令除了监听DOM事件之外,还可以用于组件之间的自定义事件。
Vue组件有类似于JavaScript中观察者模式的设计模式,可以触发事件和监听事件。在Vue中,子组件触发事件用到的是$ emit() ,父组件监听子组件的事件用到的是$ on()。父组件也可以直接在子组件的自定义标签上使用v-on监听子组件触发的自定义事件。
我们用购物车通过按钮操作商品数量的例子来说明:
首先我们来分析一下需求:
- 我们需要得到的格式是:商品名称 + 数字 -
- 其中+是按钮,每次按+数字加;- 是按钮,每次按 - 数字减1
- 父组件能够获得子组件中数字的具体值
其次我们来分析一下逻辑:
1、用户点击子组件按钮,触发子组件的按钮点击事件
- 更新逻辑(this.counter++)
- 分发事件-数据,把分发的内容保留在子组件的对象实例
2、父组件触发了被子组件分发的事件
- 处理方法中的参数,获得子组件的分发事件的数据
- 父组件获得子组件数据后的具体处理过程
<div id="app">
<custom-cart @add="handleAdd" @reduce="handleReduce" product="商品"></custom-cart>
购物车商品数量:{{total}}
</div>
<script>
Vue.component('custom-cart',{
props:['product'],
data:function(){
return {
quantity:1
}
},
template:'<div>{{product}}<button @click="reduce">-</button>{{quantity}}' +
'<button @click="add">+</button></div>',
methods:{
add:function(){
this.quantity++;
this.$emit('add',this.quantity);
},
reduce:function(){
this.quantity--;
this.$emit('reduce',this.quantity);
}
}
});
var app = new Vue({
el:'#app',
data:{
total:1
},
methods:{
handleAdd:function (quantity) {
this.total = quantity;
},
handleReduce:function(quantity){
this.total = quantity;
}
}
})
</script>
上面示例中,子组件有两个按钮,分别实现加1和减1的效果,在改变组件的data“total”后,通过$emit()再把它传递给父组件,父组件用v-on:add和v-on:reduce(示例中使用的是语法糖)。
$emit() 方法的第一个参数是自定义事件的名称,第二个参数是要传递的数据,可以选择填多个或者不填。这两个参数组成一个键值对,在子组件向父组件分发数据时,直接调用自定义事件名称下的方法就可以了。子组件触发add和reduce,然后调用handleAdd和handleReduce方法,才可以加1减1。
三、v-model在组件中的使用
在之前我们就讲过,v-model是用来传递双向数据流的指令,在组件中,v-model可以进行数据的双向绑定。但是实现双向绑定的v-model组件要满足两个条件:
- 接收一个value属性
- 在有新的value时触发input事件
<div id="app">
<custom-input v-model="inputVal"></custom-input>
{{inputVal}}
</div>
<script>
Vue.component('custom-input',{
model:{
props:'value',
event:'input'
},
props:['value'],
template:'<input :value="value" @input="update"/>',
methods:{
update:function (event) {
this.$emit('input',event.target.value);
}
}
});
var app = new Vue({
el:'#app',
data:{
inputVal:'test'
}
})
</script>
上述例子中,我们在输入框中修改value值,就可以看到输入框外的值改变。
在这段代码中,子组件向父组件分发数据和父组件向子组件传递数据这两个过程中,没有对赋值的数据做逻辑描述,因为v-model将它们进行了双向绑定,只是在update方法中,我们将’input’值赋成已改变的值,等待父组件的调用。
我们来明确一下v-model的逻辑:
1) 当父组件向子组件传值时,通过v-model传给 子组件 model里定的 props对应的属性;
2) 当子组件向父组件传值是,通过v-model传给 子组件 model里定的event对应的事件名称input,在$emit分发的事件中找到对应内容,相当于(父组件)触发对应的分发事件
不仅是input可以实现双向绑定,select的自定义事件也可以进行双向绑定:
<div id="app">
<cst-select v-model="selectValue"></cst-select>
{{selectValue}}
</div>
<script>
Vue.component('cst-select', {
model:{
prop:'selected', /*父组件给子组件传值的属性*/
event:'change' /*子组件给父组件传值时使用的分发事件名称*/
},
props: ['selected'],
template: '<select :value="selected" @change="update">' +
'<option value="1">天津</option>' +
'<option value="2">北京</option>' +
' </select>'
, methods:{
update:function (event) {
this.$emit('change',event.target.value);
}
}
});
var app = new Vue({
el:'#app',
data:{
selectValue:1
}
})
</script>
而且checkBox的自定义事件也可以进行双向绑定:
<div id="app">
<custom-checked v-model="checkedValue"></custom-checked>
{{checkedValue}}
</div>
<script>
Vue.component('custom-checked', {
model:{
props:'value',
event:'change'
},
props: ['value'],
template: '<input type="checkbox" :checked="value" @change="update">' +
'</input>',
methods:{
update:function (event) {
this.$emit('change',event.target.checked );
}
}
});
var app = new Vue({
el:'#app',
data:{
checkedValue:true
}
})
</script>
四、非父子组件通信
在实际业务中,除了父子组件通信外,还有许多非父子组件通信的场景,非父子组件一般有两种,兄弟组件和跨多级组件。
我们推荐使用一个空的Vue实例作为中央时间总线(bus),也就是一个中介,就像你和出租者之间的房屋中介一样,用下面的代码进行举例:
<div id="app">
{{message}}
<component-a></component-a>
</div>
<script>
var bus = new Vue();
Vue.component('component-a',{
template:'<button @click="handleEvent">传递事件</button>',
methods:{
handleEvent:function () {
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的事件,进而在回调中完成自己的业务逻辑。此时component-a和mounted钩子函数不是父子组件,但是它们通过bus中介完成了业务逻辑,可以正常使用$ on()和$ emit()。
下一部分我们会讲组件的插槽以及组件部分的综合案例,敬请期待!