# Vue 组件之间的通信方式
# props 和 $emit
适用于父子组件之间互相通信
详见 Vue 组件基础
# $emit 与 $on (事件总线)
适用于父子、隔代、兄弟组件通信
# 方法
- 在全局注册一个空的 Vue 实例,比如名为:
EventBus
。 - 在需要通信的组件中调用该空实例的
EventBus.$emit
触发事件,EventBus.$on
监听触发的事件。
# 示例
- 功能
功能是完成兄弟组件之间的通信:A => C, B => C
效果展示
全局注册空的 Vue 实例,并创建根组件。
// 在全局注册一个空的 Vue 实例
const EventBus = new Vue();
// 根组件
const vm = new Vue({
el: '#test',
components: {
'test-a': A,
'test-b': B,
'test-c': C,
}
});
2
3
4
5
6
7
8
9
10
11
- 配置上面局部注册的子组件
HTML 部分
<div id="test">
<test-a></test-a>
<test-b></test-b>
<test-c></test-c>
</div>
2
3
4
5
JS 部分
// 组件 A
const A = {
template:`
<div>
<p>Compontent A</p>
<button @click="sendMsg">A To C</button>
</div>
`,
data () {
return {
valueA: 'message from A'
};
},
methods: {
sendMsg () {
// 在实例 EventBus 上利用 $emit 注册自定义事件,名为 aSend
EventBus.$emit('aSend', this.valueA);
}
}
};
// 组件 B
const B = {
template:`
<div>
<p>Compontent B</p>
<button @click="sendMsg">B To C</button>
</div>
`,
data () {
return {
valueB: 'message from B'
};
},
methods: {
sendMsg () {
// 在实例 EventBus 上利用 $emit 注册自定义事件,名为 bSend
EventBus.$emit('bSend', this.valueB);
}
}
};
// 组件 C
const C = {
template:`
<div>
<p>Compontent C</p>
<div>From other component: {{ valueC }}</div>
</div>
`,
data () {
return {
valueC: ''
};
},
// 使用空 Vue 实例 EventBus 中的 $on() 监听当前实例(即:EventBus 实例)上的自定义事件
// 也就是前面利用 $emit 注册的事件:aSend 和 bSend
mounted () {
EventBus.$on('aSend', (value) => {
this.valueC = value
});
EventBus.$on('bSend', (value) => {
this.valueC = value
});
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
从上例很容易看出
利用事件总线这个方案很巧妙,通过全局注册的空 Vue 实例,各个需要通信的组件中,发送方调用空实例的$emit
,接收方调用空实例的 $on
即:在这个空 Vue 实例身上
- 利用
$emit()
注册自定义事件,并利用其第二个参数承载需要发送的数据。 - 利用
$on()
监听注册的自定义事件,并利用其第二个参数(callback)处理$emit()
第二个参数的数据。
# $attrs 与 $listeners
Vue v2.4 中新增的两个实例 property
,可用于隔代组件之间的数据传递。
# $attrs
1. 功能描述
- 用于向下隔代通信
- 首先在父组件上绑定的一个属性,欲将此数据传递给子组件,但是子组件并没有按套路 props
接收,那就会自动的被子组件的 $attrs
实例属性接收。
- 再通过父传子或者直接通过 v-bind="$attrs"
来将此数据传递给该子组件的下一代组件,数据流从第一代组件 => 第三代组件,俗称: "隔代通信"。
2. $attrs 是一个对象
$attrs
为一个对象里面以键值对的形式存着父组件传递过来并且没被 props
接收的数据。
3. 示例
欲从 Grandpa
隔代传递数据到 Grandson
数据流图

第一代组件
// 根组件
const vm = new Vue({
el: '#test',
// 这里欲将数据传递到隔代组件,要先经过第二代组件
template: `
<div>
<p>Grandpa</p>
<button @click="sendMsg">Grandpa To Grandson</button>
<son :fromGrandpa="value"></son>
</div>
`,
data: {
value: {}
},
methods: {
sendMsg () {
this.value = 'message from grandpa';
}
},
components: {
son: Son
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
第二代组件
介绍
这里利用了 Object.keys()
API,详见:MDN: Object.keys() (opens new window)。
- 功能:将一个对象的所有可枚举键名存到一个数组中。(注意深度为一,即属性为引用类型内部无法作用到)
- 参数:欲转换的对象。
- 返回值:一个表示给定对象的所有可枚举属性的字符串数组。
// 第二代组件
const Son = {
template:`
<div>
<p>Son</p>
<p>Son' $attrs = {{ this.$attrs[Object.keys(this.$attrs)[0]] }}</p>
<grandson :fromGrandpa="$attrs"></grandson>
</div>
`,
components: {
grandson: Grandson
},
updated () {
console.log(this.$attrs);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
注意
- 在这里使用
Object.keys()
是因为$attrs
为一个对象形式,这个对象内部存储着从父组件传递过来没被props
接收的数据,数据是键值对的形式。而我们只需要这个对象中的其中一个键值。 - 第二代 ==> 第三代
- 这里利用父传子将
$attrs
传递给第三代组件,第三代组件中利用props
接收此数据。 - 也可以直接通过
v-bind="$attrs"
来将此数据传递给下一代组件,这样的话props
就接收不到,所以在第三代组件上存到了$attr
里面,就得从$attrs
里取了。
- 这里利用父传子将
第三代组件
// 第三代组件
const Grandson = {
template:`
<div>
<p>Grandson</p>
<div>Grandson's $attrs: {{ fromGrandpa[Object.keys(this.fromGrandpa)[0]] }}</div>
<div>From Grandpa: {{ fromGrandpa[Object.keys(this.fromGrandpa)[0]] }}</div>
</div>
`,
props: ['fromGrandpa'],
};
2
3
4
5
6
7
8
9
10
11
💯读到这里有读者可能会说,这种方式不就是从第一代组件传数据到下一代,再从下一代在向下传一代,经历过两次父传子吗?是的,的确是这样的,只不过不同点是使用了 $attrs
来自动存储没被 props
接收的数据。
# $listeners
子组件通过 $emit()
触发的事件,都会记录在父组件的 $listeners
中,在父组件中就可以通过 v-on=$listeners
的方式继续向上一层组件传递。
1. $listeners 是个对象
$listeners
为一个对象里面存着子组件 $emit()
过来方法。
2. 示例
使用方法与 $attrs
类似
数据流图

第三代组件
联想子组件到父组件通信,利用 $emit
。
// 第三代组件
const Grandson = {
template:`
<div>
<p>Grandson</p>
<button @click="$emit('fromGrandson', 'msg from grandson')">To Grandpa</button>
</div>
`
};
2
3
4
5
6
7
8
9
第二代组件
利用 v-on="$listeners"
记录
// 第二代组件
const Son = {
template:`
<div>
<p>Son</p>
<grandson v-on="$listeners"></grandson>
</div>
`,
components: {
grandson: Grandson
},
updated () {
// 可知 $listeners 为一个对象里面存着子组件 $emit() 过来方法
console.log(this.$listeners);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
第一代组件
数据到达目标组件,这里就用传统的子传父,利用自定义事件接收传递过来的这个事件 fromGrandson
// 根组件
const vm = new Vue({
el: '#test',
template: `
<div>
<p>Grandpa</p>
<p>Message: {{ value }}</p>
<son @fromGrandson="changeValue"></son>
</div>
`,
data: {
value: ''
},
methods: {
changeValue (msg) {
this.value = msg;
}
},
components: {
son: Son
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# $parents/$children 与 ref
# $parents/$children
这个方法就很爽了,首先观察一下我们的 vm
实例。

$parents
保存有指向该组件实例的父组件实例的指针。{ Vue instance }
$children
保存有指向该组件实例的子组件实例的指针。{ Array }
注意
Vue instance
字面意思为 Vue 实例,我是按Object
理解的。- 两个属性都是明确标注只读的,
- 【TODO】是不是指针存疑,按指针理解行得通。
这就省事了,需要取值的时候利用 $parents
召唤父组件,利用 $children
召唤子组件,比较适合用于父子组件之间进行通信,当然使用套娃手段层层召唤进行通信也是可以,当然我们处理隔代、任意组件之间通信有更好的方法。
注意
这里用的是取值,指的是召唤 $parents/$children
并拿它内部的值来用,这实际意义上并不算"传值",说"拿值"比较贴切。
看一个例子
数据流图

// 子组件 A
const SonA = {
template:`
<div>
<p>Son-A</p>
<button @click="getFaMsg">get fater's msg</button>
<p>show the msg from Fa: {{ fromFaVal }}</p>
</div>
`,
data () {
return {
sonVal: 'msg from Son-A',
fromFaVal: ''
};
},
methods: {
getFaMsg () {
// $parent { Object } 指向该组件实例的父组件实例
// 直接召唤父组件来调它内部的数据
this.fromFaVal = this.$parent.value;
}
},
mounted () {
console.log(this.$parent);
}
};
// 子组件 B
const SonB = {
template: `
<div>
<p>Son-B</p>
<p>Son-B' value: {{ sonVal }}</p>
<button @click="getFaMsg">get fater's msg</button>
<p>show the msg from Fa: {{ fromFaVal }}</p>
</div>
`,
data() {
return {
sonVal: 'msg from Son-B',
fromFaVal: ''
};
},
methods: {
getFaMsg() {
this.fromFaVal = this.$parent.value;
}
},
mounted () {
// $parent { Object } 指向该组件实例的父组件实例
console.log(this.$parent);
}
};
// 根组件
const vm = new Vue({
el: '#test',
template: `
<div>
<p>Father</p>
<button @click="getSonValA">get msg from Son-A</button>
<button @click="getSonValB">get msg from Son-B</button>
<p>the value from son: {{ fromSonVal }}</p>
<son-a></son-a>
<son-b></son-b>
</div>
`,
data: {
value: 'this is a msg from father',
fromSonVal: ''
},
methods: {
getSonValA () {
this.fromSonVal = this.$children[0].sonVal;
},
getSonValB() {
this.fromSonVal = this.$children[1].sonVal;
}
},
components: {
'son-a': SonA,
'son-b': SonB
},
mounted () {
// $children { Array } 存储该组件实例对象的子组件实例对象
// 存储顺序为子组件 mounted 的顺序
console.log(this.$children);
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# ref 与 $refs
这个跟上面类似,都是数据直接从指定组件中拿数据来用。这个适用于父组件从下级子组件拿数据(当然,套娃战术层层获取也可以)
ref
给子组件注册,相当于给了该子组件一个id
,方便通过这个id
来找到该子组件从其中拿数据。$refs
一个对象,持有注册过ref
的所有 DOM 元素和组件实例, 键名为注册ref
时的值
看一个例子
数据流图

// 子组件 A
const SonA = {
template:`
<div>
<p>Son-A</p>
<p>{{ sonVal }}</p>
</div>
`,
data () {
return {
sonVal: 'msg from Son-A',
};
}
};
// 子组件 B
const SonB = {
template: `
<div>
<p>Son-B</p>
<p>{{ sonVal }}</p>
</div>
`,
data() {
return {
sonVal: 'msg from Son-B',
};
}
};
// 根组件
const vm = new Vue({
el: '#test',
// 给两个不同的组件注册不同的 ref,用于区分。
template: `
<div>
<p>Father</p>
<button @click="getSonValA">get msg from Son-A</button>
<button @click="getSonValB">get msg from Son-B</button>
<p>the value from son: {{ fromSonVal }}</p>
<son-a ref="sonA"></son-a>
<son-b ref="sonB"></son-b>
</div>
`,
data: {
fromSonVal: ''
},
methods: {
// 通过 $refs 找到注册有不同 ref 的组件,并从中拿值。
getSonValA () {
this.fromSonVal = this.$refs.sonA.sonVal;
},
getSonValB() {
this.fromSonVal = this.$refs.sonB.sonVal;
}
},
components: {
'son-a': SonA,
'son-b': SonB
},
mounted () {
console.log(this.$refs);
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# provide / inject
这两个 API 很强
provide
- 功能:在父组件上使用,内部为可以被下级所有组件共享的数据
- 类型:为一个对象或返回一个对象的函数
{ Object | () => Object }
。
inject
- 功能:在下级组件中使用,用于接受上级组件中
provide
里面共享的数据。 - 类型:一个字符串数组或者一个对象。
- 功能:在下级组件中使用,用于接受上级组件中
注意
provide
和inject
主要在开发高阶插件/组件库时使用。比如:element-ui 的tabs
和select
,但并不推荐用于普通应用程序代码中。provide
和inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
看一个例子
数据流图

// 子组件 A
const SonA = {
template:`
<div>
<p>Son-A</p>
<button @click="getFa">get msg from Fa</button>
<p>show: {{ msgFromFa }}</p>
</div>
`,
data () {
return {
msgFromFa: ''
};
},
methods: {
getFa () {
this.msgFromFa = this.faMsg;
}
},
// 子组件利用 inject 来接收
inject: ['faMsg'],
created () {
// 虽然给了个数组,但是是个对象
console.log(this.$options.inject);
}
};
// 根组件
const vm = new Vue({
el: '#test',
// 给两个不同的组件注册不同的 ref,用于区分。
template: `
<div>
<p>Father</p>
<p>Father's msg: {{ msg }}</p>
<son-a></son-a>
</div>
`,
data: {
msg: 'this is a msg from father'
},
// 该对象可被其下级注册有 inject 的所有子孙组件共享
provide: {
faMsg: 'this is a msg from father'
},
components: {
'son-a': SonA
},
created () {
// 虽然给了个对象,但是打印一下 provide 是个函数
console.log(this.$options.provide);
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
下面是两者的打印,感兴趣的童鞋可以研究一下。

# 通过 vuex
这个不多讲了,用起来很舒服,项目中频繁用到。