前言

组件是 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
<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
<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
<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>
  • 当我们点击发布的时候,所有人都能收到消息:

事件总线

结语:

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

  • 父子通信

  • 兄弟通信

  • 跨级通信

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