一、bus模式简介

1. bus简例

bus是一种通过事件实现组件交互的通信模式,它借助一个额外的Vue实例作为事件管理中心。任何引入了该Vue实例的组件都处于同一个事件环路内,可以相互注册和触发事件。一个简单的bus实例如下:
bus.js

import Vue from 'vue';

const bus = new Vue();

export default bus;

component1.vue

...
import bus from './bus.js';
bus.$on('move', (payload) => { 
  ... 
})

component2.vue

import bus from './bus.js';
...
bus.$emit('move', payload);

这里bus是一个空Vue实例,我们在component1和component2中均引入了这个实例,并在component1中注册了对move事件的监听。接下来我们在component2的合适时机下触发move事件,这样component1就可以接收到这个事件,并触发对应的回调。

如果bus.$on注册在组件内部(如methods或各个生命周期函数内),由于箭头函数内部没有this,因此你可以在回调函数内直接通过this访问当前组件实例(如果回调函数是常规函数,可以将组件实例保存在一个局部变量内)。

那么为什么需要这样一个模式呢?

2. 为什么需要bus?

这是因为,通常来说,在不借助bus模式的情况下,事件的触发只会发生在父子组件之间,并且只能由子组件向父组件触发事件。它的大致实现模式如下:
parent.vue

<template>
  <child
    @tick="handleTick"
  ></child>
</template>

<script>
export default {
  methods: {
    handleTick () { ... }
  }
}
</script>

child.vue

...
this.$emit('tick');
...

父组件向子组件绑定了一个对tick事件的监听,并定义了处理函数,而子函数可以在恰当的时机向父组件触发该事件。这种模式在封装第三方组件时极其常用。

另一种不太常见的交互是,由父组件直接调用子组件内的方法:
parent.vue

<template>
  <child ref="child"></child>
</template>

<script>
  export default {
    mounted () {
      this.$refs.child.tick();
    }
  }
</script>

这里父组件通过ref属性拿到了对子组件的引用,然后直接调用了子组件内定义的tick方法。当然,真正的函数调用仍然发生在子组件内。这种交互不涉及事件。

上述两种方法都只能作用于父子组件之间,对于无直接父子关系的组件则束手无策,这也是bus模式的使用背景。

二、bus的应用

1. bus原理

上文的例子中只展示了bus模式的简单用法,在介绍其他用法之前,我们先通过一张图来理解何为bus模式:
在这里插入图片描述
由于bus是一个完整的Vue实例,因此它具备事件管理能力。我们在任意组件内导入bus,并通过bus.$on注册一个事件监听时,该回调函数就会进入bus内的事件队列。任何引入了bus的组件,都可以订阅任何感兴趣的事件(即注册回调事件)。

事件的触发是同样的道理,我们只需要在组件内引入bus,然后通过bus.$emit触发一个事件即可。触发了一个事件后,事件队列中的所有回调函数都会按照注册顺序依次执行。

需要注意的是,回调函数的注册和执行其实都是发生在bus实例上。因此,如果所传入的回调函数是普通函数,那么函数内的this将指向bus实例,而不是注册事件的那个组件实例:

...
bus.$on('move', function(pos){
  this.pos = pos;
})

在这里插入图片描述
使用箭头函数时不会有这个问题,它内部的this指向当前组件实例:

let temp = 123;
bus.$on('move', pos => {
  this.pos = pos;
  console.log(temp);
})

我在这里特意定义了一个变量temp来说明问题。我们知道,通过作用域链,函数内可以访问外部的变量temp。同样的,由于箭头函数不会重新定义this,因此函数内的变量this可以通过作用域链拿到外部this。

2. bus的使用

理解了bus模式的原理后,它的使用就非常简单了。任何需要共享某些事件的组件只需引入同一个bus,就可以注册和触发共享的事件:
component1.vue

<template>
  <button @click="handleClick">点击我</button>
</template>
<script>
import bus from './bus.js';
export default {
  methods: {
    handleClick () {
      bus.$emit('click');
    }
  },
  mounted () {
    bus.$on('move', pos => {
      ...
    });

    bus.$on('tick', payload => {
      ...
    });
  }
}

component2.vue

<template>
  <div @mousemove.native="handleMove"></div>
</template>
<script>
export default {
  methods: {
    handleMove () {
      bus.$emit('move');
    }
  },
  mounted () {
    bus.$on('click', () => {
      ...
    })
  }
}

用一张图来表示上述关系:
在这里插入图片描述
bus允许任意多的组件参与到这个事件环路内,他们共享同一组事件队列。

另外,如果事件关系很复杂,创建多个互不相关的bus也是可以的:

src
  |-- bus
    |-- clickBus.js
    |-- moveBus.js
    ... 

这里我们创建了多个bus,clickBus专门用来管理点击相关的事件,moveBus专门管理与移动相关的事件。需要注册或触发相应的事件时,引入对应的bus即可。

总结

一般来说,bus不应该被大规模用于项目中。因为bus内事件的注册和触发分别位于不同的组件内,不便于跟踪,这在一定程度上会带来调试上的困难。

从实现原理上来说,Vuex的store模式其实是bus模式的一种封装和变体,它也是借助一个额外的Vue实例实现的:
在这里插入图片描述
不同的是,bus模式专注于事件管理,而store模式专注于数据管理。

一般来说,Vue推荐开发者多关注业务逻辑(即数据),事件应该由store直接管理,而不是发生在两个不相关的组件之间。这也是为什么Vuex专注于数据管理,而不是事件管理。不过对我们来说,在恰当的时候使用bus模式,却有可能收到意想不到的效果。

Logo

前往低代码交流专区

更多推荐