Vue组件通信-广播与派发

组件的通信 2:派发与广播 dispatch 和 broadcast 方法

provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。然后有两种场景它不能很好的解决:

  • 父组件向子组件(支持跨级)传递数据;
  • 子组件向父组件(支持跨级)传递数据。

这种父子(含跨级)传递数据的通信方式,Vue.js 并没有提供原生的 API 来支持,而是推荐使用大型数据状态管理工具 Vuex,而我们之前已经介绍过 Vuex 的场景与在独立组件(或库)中使用的限制。我们可采用已经废弃的apidispatchbroadcast实现父子组件间通信的方法

$on 与 $emit

$emit 会在当前组件实例上触发自定义事件,并传递一些参数给监听器的回调,一般来说,都是在父级调用这个组件时,使用 @on 的方式来监听自定义事件的,比如在子组件中触发事件:

1
2
3
4
5
6
7
8
// child.vue,部分代码省略
export default {
methods:{
handleEmitEvent (){
this.$emit('test','hello vue.js')
}
}
}

在父组件中监听由 child.vue 触发的自定义事件 test

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- parent.vue,部分代码省略-->
<template>
<child-component @test="handleEvent"></child-component>
</template>
<script>
export default {
methods: {
handleEvent (text){
console.log(text) // hello vue.js
}
}
}
</script>

这里是在父组件 parent.vue 中绑定的自定义事件 test 的处理句柄,然而事件 test 并不是在父组件上触发的,而是在子组件 child.vue 里触发的,只是通过 v-on 在父组件中监听。既然是子组件自己触发的,那它自己也可以监听到,这就要使用 $on 来监听实例上的事件,换言之,组件使用 $emit 在自己实例上触发事件,并用 $on 监听它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<button @click="handleEmitEvent">触发自定义事件</button>
</div>
</template>
<script>
export defalut {
methods: {
handleEmitEvent (){
// 在当前组件上触发自定义事件 test,并传值
this.$emit('test', 'Hello Vue.js')
}
},
mounted (){
// 监听自定义事件 test
this.$on('test', (text)=>{
window.$alert(text)
})
}
}
</script>

$on 监听了自己触发的自定义事件 test,因为有时不确定何时会触发事件,一般会在 mountedcreated 钩子中来监听。虽然看起有些多余,因为大可在 handleEmitEvent 里直接写 window.alert(text)

之所以多此一举,是因为 handleEmitEvent 是当前组件内的 <button> 调用的,如果这个方法不是它自己调用,而是其它组件调用的,那么就可以在其他组件改变这个组件的状态。

Vue.js 1.x 的 $dispatch 与 $broadcast

在 Vue.js 1.x 中,提供了两个方法:$dispatch$broadcast ,前者用于向上级派发事件,只要是它的父级(一级或多级以上),都可以在组件内通过 $on (或 events,2.x 已废弃)监听到,后者相反,是由上级向下级广播事件的。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 子组件 -->
<template>
<button @click="handleDispatch">派发事件</button>
</template>
<script>
export default {
methods: {
handleDispatch (){
this.$dispatch('test','Hello, Vue.js')
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 父组件,部分代码省略 -->
<template>
<child-component></child-component>
</template>
<script>
export default {
mounted (){
this.$on('test',(text)=>{
console.log(text); // Hello, Vue.js
})
}
}
</script>

$broadcast 类似,只不过方向相反。这两种方法一旦发出事件后,任何组件都是可以接收到的,就近原则,而且会在第一次接收到后停止冒泡,除非返回 true。
知道了 $dispatch$broadcast 的前世今生,我们可以尝试 Vue.js 2.x 中自行实现这两个方法。

自行实现 dispatch 和 broadcast 方法

通过目前已知的信息,我们要实现的 dispatch 和 broadcast 方法,将具有以下功能:

  • 在子组件调用 dispatch 方法,向上级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该上级组件已预先通过 $on 监听了这个事件;
  • 相反,在父组件调用 broadcast 方法,向下级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该下级组件已预先通过 $on 监听了这个事件。

实现这对方法的关键点在于,如何正确地向上或向下找到对应的组件实例,并在它上面触发方法。因为 Vue.js 内置的方法,才是以 $ 开头的,比如 $nextTick$emit 等,为了避免不必要的冲突并遵循规范,这里的 dispatch 和 broadcast 方法名前不加 $。并且该方法可能在很多组件中都会使用,复用起见,我们封装在混合(mixins)里。那它的使用样例可能是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 部分代码省略
import Emitter from '../mixins/emitter.js'

export default {
mixins: [ Emitter ],
methods: {
handleDispatch () {
this.dispatch(); // ① 三个参数 componentName, eventName, params
},
handleBroadcast () {
this.broadcast(); // ②
}
}
}

实现这对方法的关键点在于准确地找到组件实例。那在寻找组件实例上,我们的“惯用伎俩”就是通过遍历来匹配组件的 name 选项,在独立组件(库)里,每个组件的 name 值应当是唯一的,name 主要用于递归组件。

emitter.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
30
31
32
33
34
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name;

if(name == componentName){
child.$emit.apply(child,[eventName].concat(params))
}else {
broadcast.apply(child,[componentName,eventName].concat(params))
}
});
}

export default {
methods: {
dispatch(componentName,eventName,params){
let parent = this.$parent || this.$root;
let name = parent.$options.name;

while(parent && (!name || name != componentName)){
parent = parent.$parent;

if(parent){
name = parent.$options.name
}
}
if(parent){
parent.$emit.apply(parent,[eventName].concat(params))
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
}

因为是用作 mixins 导入,所以在 methods 里定义的 dispatch 和 broadcast 方法会被混合到组件里,自然就可以用 this.dispatchthis.broadcast 来使用。

这两个方法都接收了三个参数,第一个是组件的 name 值,用于向上或向下递归遍历来寻找对应的组件,第二个和第三个就是上文分析的自定义事件名称和要传递的数据。

可以看到,在 dispatch 里,通过 while 语句,不断向上遍历更新当前组件(即上下文为当前调用该方法的组件)的父组件实例(变量 parent 即为父组件实例),直到匹配到定义的 componentName 与某个上级组件的 name 选项一致时,结束循环,并在找到的组件实例上,调用 $emit 方法来触发自定义事件 eventName。broadcast 方法与之类似,只不过是向下遍历寻找。

来看一下具体的使用方法。有 A.vueB.vue 两个组件,其中 B 是 A 的子组件,中间可能跨多级,在 A 中向 B 通信:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- A.vue -->
<template>
<button @click="handleClick"></button>
</template>
<script>
import Emitter from '../mixins/emitter.js'

export default {
name: 'componentA',
mixins: [Emitter],
methods: {
handleClick (){
this.broadcast('componentB','on-message','Hello Vue.js')
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
// B.vue
export default {
name: 'componentB',
created (){
this.$on('on-message',this.showMessage)
},
methods: {
showMessage (text) {
window.alert(text);
}
}
}

同理,如果是 B 向 A 通信,在 B 中调用 dispatch 方法,在 A 中使用 $on 监听事件即可。

以上就是自行实现的 dispatch 和 broadcast 方法, 相比Vue.js 1.x,有以下不同:

  • 需要额外传入组件的 name 作为第一个参数;
  • 无冒泡机制;
  • 第三个参数传递的数据,只能是一个(较多时可以传入一个对象),而 Vue.js 1.x 可以传入多个参数,当然,你对 emitter.js 稍作修改,也能支持传入多个参数,只是一般场景传入一个对象足以。
-------------本文结束感谢您的阅读-------------