BubblyPoker's Blog

Vue2.x怎么实现组件间通信?

本文介绍几种在Vue2.x中实现组件间通信的方法,方法均来自自己平时写项目时的总结

  1. v-on/$emit
  2. 集中式状态管理 Vuex
  3. Vue事件中心
  4. 基于组件树结构的事件流

下面我就来一一介绍一下各个方法的用法(重点介绍一下后两种方法)

v-on/$emit

最基本的父子组件间通信,缺点就是如果是跨多层父子组件或者兄弟组件的话,这个方法会显得很笨拙

v-on可以用在普通元素和自定义元素组件上,在普通元素上就是监听原生的DOM事件,在自定义组件上就是监听子组件出发的自定义事件

1
2
3
4
// 在父组件中
<child-component @custom-event="handleEvent"></child-component>
// 在子组件中
this.$emit('custom-event' [, args])

集中式状态管理 Vuex

使用官方生态里的Vuex可以很方便地实现多个组件共享状态

Vuex的原理就是把各个组件的共享状态提取了出来,以一个全局单例模式管理

Vuex的用法请参考官网,官网介绍的很详细

Vue事件中心

其实这种方法原理很简单,就是把各组件间的事件通信全部都抽取出来,由一个空的Vue实例new Vue()来管理,
各组件均可以在这个空实例上面监听事件以及触发事件

具体用法如下

1
2
// 将空Vue实例挂在Vue的原型上,这样组件可以直接使用
Vue.prototype.$eventBus = new Vue()

比如A组件需要传递数据给B组件

1
2
3
4
5
6
7
// B组件中,在created或者mounted中监听事件
this.$eventBus.$on('click', (msg) => {
console.log(msg)
})
// A组件中
this.$eventBus.$emit('click', 'A is clicked')

无论A,B组件是兄弟组件还是父子组件,都可以完成组件的通信(感觉这种方式和Vuex很像呀)

但是同样缺点也很明显,就是“一方触发,八方执行”,即所有监听了click事件的回调都会在A组件触发click后执行

突然想到了一个很形象的例子:

假如现在有A,B,C,D四位同学,D向A,B,C三位同学都借了钱,有一天他们四个用QQ群视频聊天,此时
D同学不知怎么地,说了一声“还钱”,结果A,B,C三位同学都听到了,这时他们三个一起询问D上次借钱
的事儿,但是D同学现在手头钱不多,而且跟A同学关系比较好,只是想先还A的钱,这就很尴尬了…(我编不下去了)

上面的例子可以用下面几行代码表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// QQ代表事件中心,实现事件监听
Vue.prototype.$eventBus = QQ
// A,B,C分别都监听了'D要还钱'事件
[A, B, C].every(people => {
people.$eventBus.$on('还钱', () => {
console.log('D快还钱')
})
return true
})
// D无意中说了一声“还钱”
D.$eventBus.$emit('还钱', '100元')
// 然后
// A: D快还钱
// B: D快还钱
// C: D快还钱
// 其实D只想先还A的钱

所以这种方式只能在一些组件通信简单的情况下使用,当项目中组件通信复杂后,单是事件名称管理都显得很吃力

基于组件树结构的事件流

此方法可以解决Vue事件中心的“一方触发,八方执行”的弊端,可以针对性地触发特定组件的事件

在Vue1.x中,有两个api,$dispatch$broadcast

$dispatch:派发事件,事件沿着父链冒泡
$broadcast:广播事件,事件向下传导给所有的后代

个人觉得这两个api用着很是方便,但是在Vue2.x中却被废弃了,官方的解释是

因为基于组件树结构的事件流方式实在是让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱。这种事件方式确实不太好,我们也不希望在以后让开发者们太痛苦。并且$dispatch$broadcast 也没有解决兄弟组件间的通信问题

所以我们可以参照Vue1.x的api源码自己写一个$dispatch$broadcast来处理组件通信

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
// 这里不能用箭头函数,因为获取不到组件this
Vue.prototype.$broadcast = function (childName, event, args) {
this.$children.forEach(child => {
let name = child.$options.name
if (name === childName) {
child.$emit.call(child, eventName, args)
return child
} else {
this.$broadcast.call(child, childName, eventName, args)
}
})
}
Vue.prototype.$dispatch = function (parentName, event, args, _transit) {
// 如果没有父实例,那么根Vue实例指向自己
let parent = this.$parent || this.$root
let name = parent.$options.name
// 如果父实例存在,而且不是我们要找的parent,继续向上传播
// 一旦找到该组件,停止向上传播
while (parent && (!name || name !== parentName)) {
parent = this.$parent || null
parent && name = parent.$options.name
}
// 如果不是是中转点就触发事件
parent && !_transit && parent.$emit.call(parent, event, args)
return parent
}
// 旁系组件间的通信
// - B
// A
// - C - D
// B与C通信,B与D通信,baseName为A的name,即分叉点
Vue.prototype.$concat = function (collateralName, baseName, event, args) {
// 先找到分叉点
let base = this.$dispatch.call(this, baseName, event, args, true)
// 在由分叉点广播
base && this.$broadcast.call(base, collateralName, event, args)
}

针对这种场景(懒得画图了),就是A为根组件,B,D为第一层父组件,C,E为第二层子组件
/ B - C
A - D - E

  1. C –> A
1
2
// C中
this.$dispatch('A的name', event, args)
  1. A –> E
1
2
// A中
this.$broadcast('E的name', event, args)
  1. C –> E
1
this.$concat('E的name', '中转点A的名字', event, args)

这种方法其实就是利用父链,子组件可以用 this.$parent 访问它的父组件。根实例的后代可以用 this.$root 访问它。父组件有一个数组 this.$children,包含它所有的子元素

使用这个方法的前提是给组件添加name属性,而且其必须唯一

另:组件添加name属性,其实就是给组件添加自定义属性name,这些自定义属性最终都会挂在vm.$options

1
2
3
4
5
6
// 可以这样添加自定义属性
export default {
customName: 'customName',
data () {},
methods () {}
}

总结

组件间通信有两种方向,一是用一个公共实例或者工具管理,二是利用父链,子链一层一层的查询

总的来说,在Vue中实现组件间通信应该还有其他方法,但目前我自己试过的只有以上几种,特地分享出来,以后遇到新的方法会继续更新

以上…

坚持原创技术分享,您的支持将鼓励我继续创作!