Vue进阶(一):Vue组件通信的几种方式

前言

组件是 vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。

针对不同的使用场景,如何选择行之有效的通信方式?这是我们所要探讨的主题。本文总结了vue组件间通信的几种方式,以通俗易懂的实例讲述这其中的差别及使用场景。

Vue组件通信

我们可以直接用脚手架建立好各个组件,做一个 有趣 的小demo来具体演示,这样或许更加容易让人理解和记忆。而且使用脚手架我们可以是vue的 server 服务,来进行实时预览。

目录结构

分别填写模板代码:

  • GrandGrandSon下面简称小红
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div class="grandgrandson">
<h2>我是曾孙子小红</h2>
</div>
</template>
<script>
export default {
name: 'GrandGrandson',
}
</script>
<style scoped>
.grandgrandson {
border: 3px solid green;
margin: 20px;
}
</style>
  • GrandsonOne下面简称小明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="grandson">
<h2>我是孙子小明</h2>
<grand-grandson/>
</div>
</template>
<script>
import GrandGrandson from './GrandGrandson'
export default {
name: 'GrandsonOne',
components: {
GrandGrandson
}
}
</script>
<style scoped>
.grandson {
border: 3px solid #F90;
margin: 20px;
}
</style>
  • GrandsonTwo下面简称小刚
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div class="grandson">
<h2>我是孙子小刚</h2>
</div>
</template>
<script>
export default {
name: 'GrandsonTwo',
}
</script>
<style scoped>
.grandson {
border: 3px solid #F90;
margin: 20px;
}
</style>
  • GrandsonThree下面简称大明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="grandson">
<h2>我是孙子大明</h2>
<p>{{broadMsg}}</p>
</div>
</template>

<script>
export default {
name: 'GrandsonThree',
}
</script>
<style scoped>
.grandson {
border: 3px solid #F90;
margin: 20px;
float: left;
}
</style>
  • ParentOne下面简称王二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div class="parent">
<h2>我是爸爸王二</h2>
<grandson-one/>
</div>

</template>
<script>
import GrandsonOne from './GrandsonOne.vue'
export default {
name: 'ParentOne',
components: {
GrandsonOne
}
}
</script>
<style scoped>
.parent {
margin: 20px;
border: 3px solid red;
}
</style>
  • ParentTwo下面简称李四
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="parent">
<h2>我是爸爸李四</h2>
<grandson-two/>
</div>
</template>
<script>
import GrandsonTwo from './GrandsonTwo'
export default {
name: 'ParentTwo',
components: {
GrandsonTwo
}
}
</script>
<style scoped>
.parent{
border: 3px solid red;
margin: 20px;
}
</style>
  • App下面简称张三
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div id="app">
<h2>我是爷爷张三</h2>
<parent-one/>
<parent-two/>
</div>
</template>
<script>
import ParentOne from './components/ParentOne.vue'
import ParentTwo from './components/ParentTwo.vue'

export default {
name: 'App',
components: {
ParentOne,
ParentTwo
}
}
</script>
<style>
#app {
border: 3px solid blue;
display: flex;
}
</style>

这样我们会获得一个模型非常形象的展示了各个组件之间的关系,像这样:

模型图

父传子

父传子十分简单,使用 props 来进行传递,比如我们现在从 张三 传递数据给 王二李四

  • 首先我们在 张三 组件中 data 给一个值,并在张三添加一个botton ,我们点击 botton 时就传递消息 ,并给他一点样式:
1
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
<template>
<div id="app">
<h2>我是爷爷张三</h2>
<button class="button" @click = 'sendSon' >发消息给儿子</button>
<parent-one :DadMsg = 'msgToSon' />
<parent-two :DadMsg = 'msgToSon' />
</div>
</template>

<script>
import ParentOne from './components/ParentOne.vue'
import ParentTwo from './components/ParentTwo.vue'

export default {
name: 'App',
components: {
ParentOne,
ParentTwo
},
data() {
return {
msgToSon: '',
}
},
methods: {
sendSon() {
this.msgToSon = '新消息:我是张三'
}
}
}
</script>

<style>
#app {
position: absolute;
border: 3px solid blue;
float: left;
}
.button {
position: absolute;
top: 23px;
left: 150px;
}
</style>
  • 然后我们需要在 王二李四 中接收并展示这个值:
1
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
<template>
<div class="parent">
<h2>我是爸爸王二</h2>
<P>{{DadMsg}}</P>
<grandson-one/>
</div>
</template>

<script>
import GrandsonOne from './GrandsonOne.vue'
export default {
name: 'ParentOne',
components: {
GrandsonOne
},
props: {
DadMsg: String
}
}
</script>

<style scoped>
.parent {
margin: 20px;
border: 3px solid red;
float: left;
}
</style>
  • 点击按钮,即可发送消息给子组件:

父传子

子传父

子传父通过发送事件来进进行。

  • 我们先给 王二 一个按钮,当点击按钮的时候,我们就发送事件。所以要给按钮绑定一个事件。
1
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
<template>
<div class="parent">
<h2>我是爸爸王二</h2>
<P>{{DadMsg}}</P>
<button @click='sendDad'>给爸爸发消息</button>
<grandson-one/>
</div>

</template>

<script>
import GrandsonOne from './GrandsonOne.vue'
export default {
name: 'ParentOne',
components: {
GrandsonOne
},
data() {
return {
msgToDad: ''
}
},
props: {
DadMsg: String
},
methods: {
sendDad() {
this.$emit('sendDad', '王二:我是你亲生的吗?')
}
}
}
</script>
<style scoped>
.parent {
margin: 20px;
border: 3px solid red;
float: left;
}
</style>
  • 同时 张三 接收这个事件,并且对消息进行一个展示:
1
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
<template>
<div id="app">
<h2>我是爷爷张三</h2>
<P>{{sonMsg}}</P>
<button class="button" @click = 'sendSon' >发消息给儿子</button>
<parent-one :DadMsg = 'msgToSon' @sendDad='getMsg' />
<parent-two :DadMsg = 'msgToSon' />
</div>
</template>

<script>
import ParentOne from './components/ParentOne.vue'
import ParentTwo from './components/ParentTwo.vue'

export default {
name: 'App',
components: {
ParentOne,
ParentTwo
},
data() {
return {
msgToSon: '',
sonMsg: ''
}
},
methods: {
sendSon() {
this.msgToSon = '新消息:我是张三'
},
getMsg(msg) {
this.sonMsg = msg
}
}
}
</script>

<style>
#app {
position: absolute;
border: 3px solid blue;
float: left;
}
.button {
position: absolute;
top: 23px;
left: 150px;
}
</style>
  • 但我们点击 给爸爸发消息 按钮的时候, 张三 就可以接收到来着 王二 的消息。

子传父

兄弟组件

兄弟组件不能直接通信,我们只需要在父元素搭个桥即可。

  • 首先我们给 王二 一个按钮,当王二点击按钮的时候,发送给 张三,通过 张三 再传递给 李四 。当然还是别忘记了,要记得用 data 来存储这个消息。
1
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
<template>
<div class="parent">
<h2>我是爸爸王二</h2>
<P>{{dadMsg}}</P>
<button @click='sendDad'>给爸爸发消息</button>
<button @click='sendBro'>给兄弟发消息</button>
<grandson-one/>
</div>
</template>

<script>
import GrandsonOne from './GrandsonOne.vue'
export default {
name: 'ParentOne',
components: {
GrandsonOne
},
data() {
return {
msgToDad: ''
}
},
props: {
dadMsg: String
},
methods: {
sendDad() {
this.$emit('sendDad', '王二:我是你亲生的吗?')
},
sendBro() {
this.$emit('sendBro', '王二:李四,咱爸说你是从网上下载的')
}
}
}
</script>
<style scoped>
.parent {
margin: 20px;
border: 3px solid red;
float: left;
}
</style>
  • 张三 接收消息,并进行传递。
1
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
<template>
<div id="app">
<h2>我是爷爷张三</h2>
<P>{{sonMsg}}</P>
<button class="button" @click = 'sendSon' >发消息给儿子</button>
<parent-one :dadMsg = 'msgToSon' @sendDad='getMsg' @sendBro='sendBro'/>
<parent-two :dadMsg = 'msgToSon' :broMsg = 'broMsg'/>
</div>
</template>

<script>
import ParentOne from './components/ParentOne.vue'
import ParentTwo from './components/ParentTwo.vue'

export default {
name: 'App',
components: {
ParentOne,
ParentTwo
},
data() {
return {
msgToSon: '',
sonMsg: '',
broMsg: ''
}
},
methods: {
sendSon() {
this.msgToSon = '新消息:我是张三'
},
getMsg(msg) {
this.sonMsg = msg
},
sendBro(msg) {
this.broMsg = msg
}
}
}
</script>

<style>
#app {
position: absolute;
border: 3px solid blue;
float: left;
}
.button {
position: absolute;
top: 23px;
left: 150px;
}
</style>
  • 李四 接收到消息,我们并对其进行一个展示。
1
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
<template>
<div class="parent">
<h2>我是爸爸李四</h2>
<P>{{dadMsg}}</P>
<P>{{broMsg}}</P>
<grandson-two/>
</div>
</template>
<script>
import GrandsonTwo from './GrandsonTwo'
export default {
name: 'ParentTwo',
components: {
GrandsonTwo
},
props: {
dadMsg: String,
broMsg: String
}
}
</script>
<style scoped>
.parent{
border: 3px solid red;
margin: 20px;
float: left;
}
</style>
  • 点击按钮

兄弟组件传值

祖先后代 provide & inject

props一层层传递,爷爷传给孙子还好,如果嵌套了五六层还这么写,感觉自己就是一个沙雕,所以这里介绍一个稍微冷门的API,provide & inject,专门用来跨层级提供数据。

参考资料:https://cn.vuejs.org/v2/api/#provide-inject

注意:提示:provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。所以我们先在 data 中创建一个对象。

  • 张三 中,先在 data 中创建一个可监听的属性,然后通过给监听按钮的点击事件,当按钮点击时,我们传值给 小红
1
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
<template>
<div id="app">
<h2>我是爷爷张三</h2>
<P>{{sonMsg}}</P>
<button class="button" @click = 'sendSon' >发消息给儿子</button>
<button class="button2" @click = 'sendGGson' >发消息给小红</button>
<parent-one :dadMsg = 'msgToSon' @sendDad='getMsg' @sendBro='sendBro'/>
<parent-two :dadMsg = 'msgToSon' :broMsg = 'broMsg'/>
</div>
</template>

<script>
import ParentOne from './components/ParentOne.vue'
import ParentTwo from './components/ParentTwo.vue'

export default {
name: 'App',
components: {
ParentOne,
ParentTwo
},
data() {
return {
msgToSon: '',
sonMsg: '',
broMsg: '',
msgToGGson: {
msg: ''
}
}
},
provide(){
return{
msgToGGson: this.msgToGGson
}
},
methods: {
sendSon() {
this.msgToSon = '新消息:我是张三'
},
getMsg(msg) {
this.sonMsg = msg
},
sendBro(msg) {
this.broMsg = msg
},
sendGGson() {
this.msgToGGson.msg = '张三:我是你祖宗'
}
}
}
</script>

<style>
#app {
position: absolute;
border: 3px solid blue;
float: left;
}
.button {
position: absolute;
top: 23px;
left: 150px;
}
.button2 {
position: absolute;
top: 23px;
left: 250px;
}
</style>
  • 小红 中我们注入,并对值进行一个展示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="grandgrandson">
<h2>我是曾孙子小红</h2>
<p>{{msgToGGson.msg}}</p>
</div>
</template>

<script>
export default {
name: 'GrandGrandson',
inject: ['msgToGGson'],
}
</script>
<style scoped>
.grandgrandson {
border: 3px solid green;
margin: 20px;
}
</style>
  • 点击按钮:

provide & inject

dispatch

递归获取 $parent 即可。原理就是通过 this.$parent 来获取父元素,并且如果没有找到并且会一直往上寻找。

  • 注意这需要放在vue的原型链之上。
  • 首先我们在 main.js 文件中添加:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

Vue.prototype.$dispatch = function(eventName, data) {
let parent = this.$parent
while(parent) {
parent.$emit(eventName, data)
parent = parent.$parent
}
}

new Vue({
render: h => h(App),
}).$mount('#app')
  • 小红 中我们添加一个按钮,监听点击事件发送我们的消息.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div class="grandgrandson">
<h2>我是曾孙子小红</h2>
<button @click= 'dispatch'>dispatch通知父元素</button>
<p>{{msgToGGson.msg}}</p>
</div>
</template>

<script>
export default {
name: 'GrandGrandson',
inject: ['msgToGGson'],
methods: {
dispatch() {
this.$dispatch('dispatch','小红:我4点钟回家')
}
}
}
</script>
<style scoped>
.grandgrandson {
border: 3px solid green;
margin: 20px;
}
</style>
  • 在所有父元素中我们先在 data 中接收数据,并在 mounted 中监听 dispatch ,以 张三 为例
1
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
<template>
<div id="app">
<h2>我是爷爷张三</h2>
<P>{{sonMsg}}</P>
<p>{{gruandsonMsg}}</p>
<button class="button" @click = 'sendSon' >发消息给儿子</button>
<button class="button2" @click = 'sendGGson' >发消息给小红</button>
<parent-one :dadMsg = 'msgToSon' @sendDad='getMsg' @sendBro='sendBro'/>
<parent-two :dadMsg = 'msgToSon' :broMsg = 'broMsg'/>
</div>
</template>

<script>
import ParentOne from './components/ParentOne.vue'
import ParentTwo from './components/ParentTwo.vue'

export default {
name: 'App',
components: {
ParentOne,
ParentTwo
},
data() {
return {
msgToSon: '',
sonMsg: '',
broMsg: '',
msgToGGson: {
msg: ''
},
gruandsonMsg: ''
}
},
provide(){
return{
msgToGGson: this.msgToGGson
}
},
methods: {
sendSon() {
this.msgToSon = '新消息:我是张三'
},
getMsg(msg) {
this.sonMsg = msg
},
sendBro(msg) {
this.broMsg = msg
},
sendGGson() {
this.msgToGGson.msg = '张三:我是你祖宗'
}
},
mounted() {
this.$on('dispatch', msg => {
this.gruandsonMsg = msg
})
}
}
</script>

<style>
#app {
position: absolute;
border: 3px solid blue;
float: left;
}
.button {
position: absolute;
top: 23px;
left: 150px;
}
.button2 {
position: absolute;
top: 23px;
left: 250px;
}
</style>
  • 当我们点击按钮时,就可以通知祖先元素。

dispatch

broadcast

dispatch 类似,递归获取 $children来向所有子元素广播。

  • main.js 文件中添加:
1
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
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

Vue.prototype.$dispatch = function(eventName, data) {
let parent = this.$parent
while(parent) {
parent.$emit(eventName, data)
parent = parent.$parent
}
}

Vue.prototype.$broadcast = function(eventName, data) {
broadcast.call(this, eventName, data)
}

function broadcast(eventName, data) {
this.$children.forEach(child => {
child.$emit(eventName, data)
if(child.$children.length) {
broadcast.call(child, eventName, data)
}
});
}

new Vue({
render: h => h(App),
}).$mount('#app')
  • 王二 中添加一个按钮,并绑定一个事件。
1
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
<template>
<div class="parent">
<h2>我是爸爸王二</h2>
<button @click='sendDad'>给爸爸发消息</button>
<button @click='sendBro'>给兄弟发消息</button>
<button @click = 'broadcast' >broadcast广播子元素</button>
<P>{{dadMsg}}</P>
<p>{{gruandsonMsg}}</p>
<grandson-three/>
<grandson-one/>
</div>
</template>

<script>
import GrandsonOne from './GrandsonOne.vue'
import GrandsonThree from './GrandsonThree'
export default {
name: 'ParentOne',
components: {
GrandsonOne,
GrandsonThree
},
data() {
return {
msgToDad: '',
}
},
props: {
dadMsg: String
},
methods: {
sendDad() {
this.$emit('sendDad', '王二:我是你亲生的吗?')
},
sendBro() {
this.$emit('sendBro', '王二:李四,咱爸说你是从网上下载的')
},
broadcast() {
this.$broadcast('broadcast', '广播:所有人今天晚上8点前回家')
}
}
}
</script>
<style scoped>
.parent {
margin: 20px;
border: 3px solid red;
float: left;
}
</style>
  • 然后在所有子组件的 mounted中监听事件接收消息,保存到 data 并进行展示。以 大明 为例:
1
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
<template>
<div class="grandson">
<h2>我是孙子大明</h2>
<p>{{broadMsg}}</p>
</div>
</template>

<script>
export default {
name: 'GrandsonThree',
data() {
return {
broadMsg: ''
}
},
mounted() {
this.$on('broadcast', msg => {
this.broadMsg = msg
})
}
}
</script>
<style scoped>
.grandson {
border: 3px solid #F90;
margin: 20px;
float: left;
}
</style>
  • 点击按钮,所有人都将收到消息

broadcast

event-bus

如果两个组件没有什么关系,我们只能使用订阅发布模式来做,并且挂载到Vue.protytype上,我们来试试,我们称呼这种机制为总线机制,也就是喜闻乐见的event-bus。

我们在Vue脚手架中只需要在 main.js 文件中添加:

1
Vue.prototype.$bus = new Vue()

当然也可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Bus {
constructor() {
this.callbacks = {}
}
$on(name, fn) {
//监听
this.callbacks[name] = this.callbacks[name] || []
this.callbacks[name].push(fn)
}
$emit(name, args) {
// 触发
if(this.callbacks[name]) {
this.callbacks[name].forEach(cb => cb(args))
}
}
}

Vue.prototype.$bus = new Bus()
  • 我们在 小明 中添加一个按钮,用来发布。
1
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
<template>
<div class="grandson">
<h2>我是孙子小明</h2>
<button @click="bus">bus发布</button>
<p>{{broadMsg}}</p>
<p>{{gruandsonMsg}}</p>
<grand-grandson/>
</div>
</template>

<script>
import GrandGrandson from './GrandGrandson'
export default {
name: 'GrandsonOne',
components: {
GrandGrandson
},
data() {
return {
broadMsg: '',
gruandsonMsg: ''
}
},
methods: {
bus() {
this.$bus.$emit('bus', '小明:小红考试没及格')
}
},
mounted() {
this.$on('broadcast', msg => {
this.broadMsg = msg
}),
this.$on('dispatch', msg => {
this.gruandsonMsg = msg
})
},

}
</script>
<style scoped>
.grandson {
border: 3px solid #F90;
margin: 20px;
float: left;
}
</style>
  • 在其他任何组件中,我们都可以接收到来自小明发布的内容,我们以 小刚 为例子:
1
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
<template>
<div class="grandson">
<h2>我是孙子小刚</h2>
<p>{{broadMsg}}</p>
<p>{{busMsg}}</p>
</div>
</template>

<script>
export default {
name: 'GrandsonTwo',
data() {
return {
broadMsg: '',
busMsg: ''
}
},
mounted() {
this.$on('broadcast', msg => {
this.broadMsg = msg
}),
this.$bus.$on('bus', msg => {
this.busMsg = msg
})
}
}
</script>
<style scoped>
.grandson {
border: 3px solid #F90;
margin: 20px;
}
</style>
  • 当我们点击发布的时候,所有人都能收到消息:

事件总线

结语:

常见使用场景可以分为三类:

  • 父子通信

  • 兄弟通信

  • 跨级通信

我们可以在不同的场景中选择最合适的方式来进行通信。

------ 本文结束  感谢阅读 ------