前言
组件是 vue.js 最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。
针对不同的使用场景,如何选择行之有效的通信方式?这是我们所要探讨的主题。本文总结了 vue 组件间通信的几种方式,以通俗易懂的实例讲述这其中的差别及使用场景。
Vue 组件通信
我们可以直接用脚手架建立好各个组件,做一个 有趣 的小 demo 来具体演示,这样或许更加容易让人理解和记忆。而且使用脚手架我们可以是 vue 的 server
服务,来进行实时预览。
分别填写模板代码:
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>
|
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>
|
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>
|
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>
|
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>
|
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>
|
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 。
注意:提示:provide
和 inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。所以我们先在 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>
|
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>
|
broadcast
和 dispatch
类似,递归获取 $children
来向所有子元素广播。
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>
|
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>
|
结语:
常见使用场景可以分为三类:
我们可以在不同的场景中选择最合适的方式来进行通信。