写在前面
3种通信模式:父子组件通信、兄弟组件通信、隔代通信
7种组件通信方式:
-
props && $emit
-
parent && root && $children
-
$attrs
-
ref
-
provide && inject
-
事件总线EventBus
-
vuex
1. props && $emit
<!-- 示例1:-->
<body>
<div id="app">
<!-- myCpn自定义组件写成my-cpn cpnList是子组件props中声明的属性写成cpn-list,接受父组件传递的属性-->
<!-- v-bind:cpn-list="list" 简写 :cpn-list="list" -->
<!-- cpn-list是子组件props中定义的属性cpnList,用于接收父组件传递来的list -->
<my-cpn :cpn-list="list" :cpn-pers-info="persInfo"></my-cpn>
</div>
<script>
Vue.component('myCpn', {
// 在子组件的模板中使用子组件props中的属性
template: `<div>
<h1>我是子组件myCpn</h1>
<h1>接收到的数据:{{cpnList}}</h1>
<h1>接收到的数据:{{cpnPersInfo}}</h1>
</div>`,
props: ['cpnList', 'cpnPersInfo']
});
var vm = new Vue({
el: "#app",
data() {
return {
list: ['数码', '家电', '旅行'],
persInfo: {
name: 'Bob',
age: 20
},
}
}
});
</script>
</body>
<!-- 示例1:-->
<body>
<!-- 子组件模板 -->
<template id="son">
<div>
<!-- 1. 子组件通过点击事件触发事件函数 -->
<button v-for="item of categories" @click="btnClick(item)">{{item.name}}</button>
</div>
</template>
<!-- 父组件模板 -->
<div id="app">
<!-- 3. 父组件通过监听子组件的自定义事件item-click进行处理,注意自定义的事件的函数没有event对象,但是这里默认会将子组件的数据item带过去 -->
<cpn-button @item-click="cpnClick"></cpn-button>
</div>
<script>
// 子组件
Vue.component('cpnButton', {
data() {
return {
categories: [
{ id: '1', name: '家电' },
{ id: '2', name: '科技' },
{ id: '3', name: '户外' },
],
}
},
template: '#son',
methods: {
btnClick(item) {
// 2. 子组件将点击获取到的数据通过 this.$emit(自定义事件名,传递给父组件的数据)发射自定义事件;自定义事件名要小写短横线连接
this.$emit('item-click', item)
}
}
})
// 父组件实例
new Vue({
el: '#app',
methods: {
// 4. 父组件监听的自定义事件函数接收到子组件传递来的item数据
cpnClick(item) {
console.log("---", item)
}
}
})
</script>
</body>
2. $parent && $root && $children
-
this.$parent:可以访问到父组件,但是不提倡这样做,这样会破坏子组件的复用性,比如在子组件中修改父组件A的name属性,但是其他B的组件引用这个子组件,B组件中并没有name属性;会导致没有解耦
-
this.$root: 会直接访问到根实例
-
this.$children:是一个包含所有直接子组件对象的数组;比如上面例子中的A、B、C;在A组件中this.children包含一个C组件;
// 在A组件中使用下面这个可以访问到C组件的msgC属性
this.$children[0].$children[0].msgC
<!-- this.$children示例 -->
<body>
<div id="app">
<cpn></cpn>
<button @click="btn">按钮</button>
</div>
<template id="cpn">
<div>
<h1>{{message}}</h1>
</div>
</template>
<script>
var vm = new Vue({
el: "#app",
data: {
message: "我是父组件app",
},
methods: {
btn() {
console.log(this.$children[0].message)
this.$children[0].show();
}
},
components: {
cpn: {
template: '#cpn',
data() {
return {
message: "我是子组件cpn",
}
},
methods: {
show() {
console.log(this.message);
}
}
}
}
});
</script>
</body>
3. $attrs组件间通信
vm.$attrs: 是一个对象,用于接受父组件传递的所有的属性。
一般我们接受父组件传递的属性都是在子组件中定义props选项接受父组件传递的值。
attrs" 传入内部组件
试想有A,B,C祖孙3代组件,我们想从A组件传递属性a到C组件,但是如果采用props的方式,就会导致在中间层B组件中定义props用于接受A组件传值a,然后再通过B组件props接受的a,传递到C组件中,但是B组件并没有用a,却还是需要定义props属性接受;如果组件嵌套层数很多的情况,中间组件就会定义大量的props导致浪费。
attrs可以很方便的做到属性透传,使用起来也比较简单,避免了中间组件多写props 的痛苦
<!-- 示例1:-->
<!-- A.vue -->
<template>
<div>
我是A组件
<B :msg-a="msgA" :list-a="listA"></B>
</div>
</template>
<script>
import B from "./B";
export default {
data () {
return {
msgA: 'hello msgA',
listA: ['五毒神掌', '黯然销魂掌', '降龙十八掌']
}
},
components: {
B
}
}
</script>
<!-- B.vue -->
<template>
<div>
我是B组件
<C v-bind="$attrs" :msg-b="msgB"></C>
</div>
</template>
<script>
import C from "./C";
export default {
data () {
return {
msgB: 'hello msgB'
}
},
components: {
C
}
}
</script>
<!-- C.vue -->
<template>
<div>我是C组件{{ msgA }} -- {{ msgB }} -- {{ listA }}</div>
</template>
<script>
export default {
props: ['msgA', 'msgB', 'listA']
}
</script>
// main.js
import Vue from "vue";
import A from "./components/A.vue";
new Vue({
render: h => h(A)
}).$mount("#app");
<!-- 示例2:注意点 -->
<!-- B.vue -->
<template>
<div>
我是B组件{{ listA }}-- {{ msgA }}
<C v-bind="$attrs" :msg-b="msgB"></C>
</div>
</template>
<script>
import C from "./C";
export default {
data () {
return {
msgB: 'hello msgB'
}
},
props: ['msgA', 'listA'],
components: {
C
}
}
</script>
<!-- 示例2总结:A透传给C的属性在$attrs里面,在透传的过程中B从$attrs中取走了A透传的listA和msgA,那么这两个listA和msgA属性就会从 $attrs中被剔除,所以C组件就无法从$attrs获取到A透传的属性了-->
4. ref
定义:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
作用:简单说就是给子组件上绑定ref,则父组件就可以通过this.$ref.子组件绑定的ref的值获取到子组件的实例.
$ref结构:默认是空对象,当在子组件上绑定ref属性时,this.$refs的结构就变成{ 绑定的ref属性: 对应的子组件 }
注意点:当ref获取组件时,组件并不是DOM元素,所以无法直接访问dom元素的属性;需要借助组件的$el获取到组件的dom元素
// this获取组件之后再通过组件的$el获取到组件的dom元素,进而访问dom属性
this.$ref.xxx.$el.style.color = red
4.1 通过ref直接访问dom元素
// 示例1:ref访问dom元素
<body>
<div id="app">
// 给p元素绑定ref属性,则父组件vm实例可以通过vm.$ref.pref引用到p元素进而操作p元素
<p ref="pref" attr="ppp">我是p元素</p>
<div>
{{ getElement() }}
</div>
</div>
<script>
var vm = new Vue({
el: '#app',
methods: {
getElement() {
// this.$refs.pref指向p元素
// vm.$refs.pref.getAttribute('attr')可以获取到p元素上的attr属性值
return this.$refs.pref
}
}
})
</script>
</body>
4.2 通过ref直接访问子组件实例
// 示例2:通过ref访问子组件实例
<div id="app">
<cpn ref="cpnRef"></cpn>
<button @click="getcpn">btn</button>
<p>{{ msg }}</p>
</div>
<script>
var vm = new Vue({
el: '#app',
data() {
return {
msg: ''
}
},
components: {
cpn: {
template: '<div>我是cpn组件</div>',
data() {
return {
cpnMsg: "我是cpn组件的cpnMsg"
}
}
}
},
methods: {
getcpn() {
// this.$refs.cpnRef指向cpn组件实例;可以简单调用cpn组件的属性或者方法
// this.$refs.cpnRef.cpnMsg 获取到cpn子组件的属性值
this.msg = this.$refs.cpnRef.cpnMsg
}
}
})
</script>
5. provide && inject
核心思想:相当于react的context。父组件在provider中暴露属性、方法、实例等,子或者孙...可以在inject中获取到父组件暴露出来的数据(属性、方法、实例等)
缺点:
- 不适合兄弟组件通信,或者两个不相干的组件通信
- 只能父组件给后代暴露,供后代使用;不能后代向父组件暴露
优点:适合父组件和嵌套层次深的孙孙...组件间的通信。
注意点:1. provider暴露的名称,必须和inject引入的名称一直;2. provider暴露方法时,必须在父组件中提前绑定好this
// 经典案例:el-form和el-form-item之间的通信,使用的就是provide && inject
<el-form label-width="80px" :model="formData">
<el-form-item label="姓名">
<el-input v-model="formData.name"></el-input>
</el-form-item>
<el-form-item label="年龄">
<el-input v-model="formData.age"></el-input>
</el-form-item>
</el-form>
基本用法:
// 父组件
<template>
<div class="father" >
<div>子组件对我说:{{ sonMes }}</div>
<div>孙组件对我说:{{ grandSonMes }}</div>
<son />
</div>
</template>
<script>
import son from './son'
export default {
name:'father',
components:{
son /* 子组件 */
},
provide(){
return {
/* 将自己暴露给子孙组件 ,这里声明的名称要于子组件引进的名称保持一致 */
father:this
}
},
data(){
return {
grandSonMes:'', /* 来自子组件的信息 */
sonMes:'' /* 发送给子组件的信息 */
}
},
methods:{
/* 接受孙组件信息 */
grandSonSay(value){
this.grandSonMes = value
},
/* 接受子组件信息 */
sonSay(value){
this.sonMes = value
},
},
}
</script>
// 子组件
<template>
<div class="son" >
<input v-model="mes" /> <button @click="send" >对父组件说</button>
<grandSon />
</div>
</template>
<script>
import grandSon from './grandSon'
export default {
/* 子组件 */
name:'son',
components:{
grandSon /* 孙组件 */
},
data(){
return {
mes:''
}
},
/* 引入父组件 */
inject:['father'],
methods:{
send(){
this.father.sonSay(this.mes)
}
},
}
</script>
// 孙组件
<template>
<div class="grandSon" >
<input v-model="mes" /> <button @click="send" >对爷爷组件说</button>
</div>
</template>
<script>
export default {
/* 孙组件 */
name:'grandSon',
/* 引入爷爷组件 */
inject:['father'],
data(){
return {
mes:''
}
},
methods:{
send(){
this.father.grandSonSay( this.mes )
}
}
}
</script>
暴露方法时绑定this
// 父组件
provide(){
return {
/* 将通信方法暴露给子孙组件(注意绑定this) */
grandSonSay:this.grandSonSay.bind(this),
sonSay:this.sonSay.bind(this)
}
},
methods:{
/* 接受孙组件信息 */
grandSonSay(value){
this.grandSonMes = value
},
/* 接受子组件信息 */
sonSay(value){
this.sonMes = value
},
},
// 子组件
/* 引入父组件方法 */
inject:['sonSay'],
methods:{
send(){
this.sonSay(this.mes)
}
},
6. 事件总线EventBus
优点: 不受组件层级的影响,可以实现任意两个组件的通信。需要数据就通过on绑定,传递数据就emit触发
缺点: 事件组件命名不利于维护
7. vuex
缺点: vuex通信方式相比其他方式,比较复杂,而且如果不同的模块,需要建立独立的modules
优点:
- 解决两个毫无干系的两个组件的通信问题
- 支持异步组件通信(vuex中actions允许我们做一些异步操作)
问题:为什么action可以实现异步?
因为里面底层通过Promise.resolve能够获取异步任务完成的状态。