Bootstrap

vue组件通信必知必会——事件总线Event Bus

一、关于vue组件通信

1)含义:Vue组件通信是指在Vue框架中,不同组件之间传递数据和触发行为的过程。

2)主要场景:父子组件通信、隔代组件通信、兄弟(跨)组件通信

3)主要方式:(以vue2为例)

  • props / $emit ,适用:父子组件通信
    props:父组件通过props属性向子组件传递数据。$emit:子组件使用$emit触发一个事件,将数据传递回父组件。父组件通过v-on@监听并响应这个事件。

  • $parent / $children,适用:父子组件通信
    vue2中可以通过直接访问this.$parent / this.$children 访问父 / 子实例。

  • ref / $refs,适用:父子组件通信
    父组件通过$ref可以直接引用子组件的方法或属性。ref如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。
    注意:但这通常用于操作子组件而不是传递数据,因为这可能导致代码紧密耦合。

  • provide / inject ,适用:隔代组件通信
    父组件可以使用provide选项来提供数据,而无需通过中间的子组件层级,子组件则通过inject选项注入这些数据。

  • Vuex / Pina 全局状态管理器,适用:父子、隔代、兄弟组件通信等复杂的场景
    当多个组件需要共享和管理同一状态时,使用Vuex可以保持数据的一致性,并提供强大的状态管理和操作工具。

  • Event Bus 事件总线,适用于:父子、隔代、兄弟组件通信等。
    Event Bus是创建一个全局Vue实例作为事件中心,组件之间可以通过$emit$on进行非父子关系的通信。Event Bus和Vuex有点类似,但Vuex更适合项目比较大且更复杂的场景。

  • $attrs / $listeners ,适用:隔代组件通信
    $attrs$listeners 是Vue中两个特殊的属性,主要用于解决组件之间的属性继承和事件监听问题,特别是在封装可复用组件时非常有用。
    $attrs :包含没有被作为prop定义的特性(attributes)。当一个组件没有声明接受某些属性时,传递给该组件的额外属性会被收集在$attrs对象中。这使得我们可以很容易地将这些未被识别的属性“透传”给内部组件。比如:如果有一个封装好的组件,想让它自动传递所有未被处理的属性给其内部的子组件,可以在组件的根元素上使用v-bind="$attrs"
    $listeners:包含了父组件绑定给当前组件的所有事件监听器。这是一个对象,每个属性对应一个事件名称,属性值为对应的事件处理函数数组。比如:想让一个组件代理所有接收到的事件到其内部的某个元素上,可以使用v-on="$listeners"。这样,任何父组件对当前组件绑定的事件监听都会被自动转发到指定元素。
    这两个特性经常一起使用,以便创建一个“透明”组件,即一个组件能够接收并传递所有未被明确处理的属性和事件给其内部的子组件,使得封装组件时更加灵活,同时保持组件的高度可复用性。

本文重点主要阐述Event Bus这种通信方式的使用。

二、什么是事件总线(Event Bus)?

事件总线(Event Bus) 是一种用于组件间通信的模式,通常用于解决组件之间的解耦和简化通信的问题。

事件总线(Event Bus) 是一个能够触发和监听事件的机制,使得组件能够在不直接依赖彼此的情况下进行通信。

事件总线(Event Bus) 可以是一个全局的单例对象,也可以是一个基于发布-订阅模式的实现。

三、关于订阅-发布模式(Publish-Subscribe Pattern)

1、构成

发布者(Publisher): 负责发布(广播)消息或事件的对象。当发布者的状态发生变化时,它会通知所有已订阅的对象。

订阅者(Subscriber): 订阅发布者的消息或事件的对象。订阅者通过注册自己的回调函数(或观察者)来接收发布者的通知。

2、实现步骤

1)发布者维护一个订阅者列表(比如:数组),用于存储所有订阅了它的对象。

2)订阅者向发布者注册自己的回调函数(或观察者)。

3)当发布者的状态发生变化时,它会遍历订阅者列表,调用每个订阅者的回调函数,通知它们状态的变化。
在这里插入图片描述

四、事件总线 优 / 缺点

1、优点:

  • 跨组件通信: 可以方便地实现非父子组件之间的通信,不需要在组件之间建立直接的关联。

  • 全局通信: 事件总线通常是全局性的,能够在整个应用中的任何地方进行通信,适用于全局状态的传递和应用的整体控制。

  • 解耦组件: 能够实现组件之间的解耦,使得组件之间不需要直接引用或依赖彼此,提高了代码的灵活性和可维护性。

  • 简化通信: 对一些简单的通信需求,事件总线提供了一种相对简单的方式,避免了通过 props 和回调函数传递数据时的繁琐操作。

2、缺点:

  • 全局状态管理: 使用事件总线可能引入全局状态,导致应用状态变得难以追踪和理解,特别是在大型应用中。

  • 难以调试: 全局性的事件监听和触发可能使得追踪代码执行流程和调试变得更加困难,尤其是在复杂的应用场景下。

  • 不明确的数据流向: 使用事件总线时,数据的流向相对不明确,可能增加代码的复杂性,使得应用程序的数据流变得更加难以理解。

  • 潜在的性能问题: 大量的全局事件监听和触发可能导致性能问题,尤其是在频繁触发事件的情况下。

  • 安全性问题: 由于事件总线是全局的,可能存在安全风险,例如某个组件监听了不应该被其它组件触发的敏感事件。

简单总结 :小型应用或简单的场景,事件总线适用,但在大型应用或需要更严格状态管理和调试的情况下,可考虑使用更复杂的状态管理工具,如 Vuex 或 Redux。

五、实现与使用

1、vue2项目

第一步:新建文件eventBus.js

const eventBus = new Vue({
  methods: {
    emit(event, ...args) { // 发布
      this.$emit(event, ...args);
    },
    on(event, callback) { // 接收
      this.$on(event, callback);
    },
    off(event, callback) { // 销毁
      this.$off(event, callback);
    }
  }
});
export default eventBus;

第二步:在main.js中全局注册

import Vue from "vue";
// ...

import eventBus from "./utils/eventBus.js"; // 引入封装的方法
// ...

Vue.prototype.$bus = eventBus; // 挂载载Vue实例上
// ...

第三步:使用
home.vue组件中发布数据

<template>
  <div class="hello">
    <h1>Home Page</h1>
    <el-button type="primary" @click="handleClick">
      点击将数据传到about组件
    </el-button>
  </div>
</template>

<script>
export default {
  data() {
    return {
    	hasSendMessage: false
    };
  },
  methods: {
    handleClick() {
      this.hasSendMessage = true;
      this.$bus.emit("fromHome", {
        msg: "this is form Home page message ~ "
      });
    }
  },
  destroyed() {
    this.hasSendMessage && this.handleClick(); // 这里很重要!!!
    this.hasSendMessage = false;
  }
};
</script>

about.vue 接收数据

<template>
  <div class="hello">
    <h1>About Page</h1>
    <div>接收到的数据: {{ message }}</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: ""
    };
  },
  created() {
    this.$bus.on("fromHome", data => {
      console.log("create receive data: ", data);
      this.message = data.msg;
    });
  },
  destroyed() {
    this.$bus.off("fromHome"); // 这里也需要注意,组件销毁时同时销毁eventBus事件,以免消耗性能
  }
};
</script>

实际结果:
请添加图片描述
可能遇到的问题:

1、接收数据的组件收到了传递的内容,但是在页面上没有展示。

  • 原因:与vue组件的加载顺序和生命周期有关。

  • 解决:在分发消息的组件destroyed()生命周期钩子函数中再调用emit() 分发消息。(上述vue2示例中,也重点标注了要注意的地方)

  • vue2 父组件和子组件的生命周期的执行顺序:

1)创建 / 挂载过程::父组件beforeCreate -> 父组件created -> 父组件beforeMount -> 子组件beforeCreate -> 子组件created -> 子组件beforeMount -> 子组件mounted -> 父组件mounted

2)更新过程:父组件数据发生变化,导致子组件需要重新渲染时 :父组件beforeUpdate -> 子组件beforeUpdate -> 子组件updated -> 父组件updated

3)销毁过程: 当父组件被销毁时:父组件beforeDestroy -> 子组件beforeDestroy -> 子组件destroyed -> 父组件destroyed

扩展至vue3:

  • Vue3中的beforeDestroy和destroyed生命周期钩子已被替换为beforeUnmount和unmounted。

  • Vue3中子组件的beforeMount和mounted钩子函数会在父组件对应的钩子函数之前执行,因为在Vue3中,子组件的setup函数执行时会先于父组件的beforeMount和mounted钩子函数。

  • vue3子组件,如果需要访问父组件的生命周期钩子函数,可以使用getCurrentInstance().parent来访问父组件的实例。

理解: 保证父组件在进行操作前,等待子组件完成相应的生命周期步骤。例如:在挂载过程中,父组件先挂载,然后子组件挂载;在更新过程中,父组件先更新,然后子组件更新;在销毁过程中,同样遵循从父到子的顺序。

这种设计允许父组件控制何时以及如何创建、更新和销毁子组件。

2、vue3项目

Vue3中默认情况下是没有内置的 EventBus,也就是说Vue3 没有像 Vue2 那样的 $on()$emit() 的全局事件系统。

因为在 Vue 3 中,官方推荐使用 Composition API 以及更灵活的函数式组件,同时提倡使用更明确的通信方式。比如propsemitsprovide/injectpinia。

一般在vue3使用eventBus功能常常借用第三方库,比如本文在接下来介绍的mitt 和 vue3-eventbus等。

方式1:使用 mitt

mitt 是一个简单而强大的事件总线库,用于在组件之间进行事件的发布和订阅。

它提供了一种简洁的方式来实现组件之间的通信,而无需借助 Pinia 或其他状态管理库。

  • 安装:npm install --save mitt

  • 新建文件:eventBus.js:

import mitt from "mitt";

const eventBus = new mitt();

export default eventBus;

mitt 主要方法:

emit(name,data) // 触发事件,两个参数:name:触发的方法名,data:需要传递的参数
on(name,callback) // 绑定事件,两个参数:name:绑定的方法名,callback:触发后执行的回调函数
off(name)  // 解绑事件,一个参数:name:需要解绑的方法名
  • 使用:

home.vue发送数据

<template>
  <h1>Home page</h1>
  <a-button type="primary" @click="handleClick">
    点击将消息发送给About组件
  </a-button>
</template>

<script lang="ts" setup>
import { onUnmounted, ref } from "vue";
import eventBus from "../utils/eventBus";

const hasClicked = ref(false);

const handleClick = function () {
  eventBus.emit("homeMsg", "this is message publish by home page. ");
  hasClicked.value = true;
};

onUnmounted(() => { // 此处代码也很重要!有可能导致接收数据的组件收不到,原因也是和vue3组件生命周期执行顺序由关
  hasClicked.value && handleClick();
  hasClicked.value = false;
});

</script>

about.vue接收数据

<template>
  <h1>About page</h1>
  <div>receive Message: {{ receiveMsg }}</div>
</template>

<script setup lang="ts">
import { ref, onBeforeMount, onUnmounted } from "vue";

import eventBus from "../utils/eventBus";

const receiveMsg = ref("");

// 此处监听的位置需要注意!
onBeforeMount(() => {
  eventBus.on("homeMsg", (content: string) => {
    receiveMsg.value = content;
    console.log("received message from home: ", content);
  });
});

onUnmounted(() => {
  eventBus.off("homeMsg");
});

</script>

效果:
请添加图片描述
方式2:使用 vue3-eventbus

安装:npm install --save vue3-eventbus

挂载:main.js

import eventBus from 'vue3-eventbus'
// ...
app.use(eventBus)

使用:
home.vue发送数据

<template>
  <h1>Home page</h1>
  <a-button type="primary" @click="handleClick">
    点击将消息发送给About组件
  </a-button>
</template>

<script lang="ts" setup>
import { onUnmounted, ref } from "vue";

import eventBus from "vue3-eventbus"; // 对比mitt使用区别,就是入口不一样而已,其他都一样

const hasClicked = ref(false);

const handleClick = function () {
  eventBus.emit("homeMsg", "this is message publish by home page. ");
  hasClicked.value = true;
};

onUnmounted(() => {
  hasClicked.value && handleClick();
  hasClicked.value = false;
});

</script>

about.vue接收数据

<template>
  <h1>About page</h1>
  <div>receive Message: {{ receiveMsg }}</div>
</template>

<script setup lang="ts">
import { ref, onBeforeMount, onUnmounted } from "vue";

import eventBus from "vue3-eventbus";

const receiveMsg = ref("");

onBeforeMount(() => {
  eventBus.on("homeMsg", (content: string) => {
    receiveMsg.value = content;
    console.log("received message from home: ", content);
  });
});

onUnmounted(() => {
  eventBus.off("homeMsg");
});

</script>

效果同使用mitt就不列举了。

vue3项目中使用注意点:要在合适的生命周期钩子中emit数据,否则很容易出现接收数据的组件中on事件监听不到数据。

  • 重要知识点 ‼️)vue3路由跳转时,页面1和页面2的生命周期执行顺序为:

旧页面onBeforeUnmount -> 新页面setup -> 新页面onBeforeMount -> 旧页面onUnmounted -> 新页面onMounted

六、总结

用eventBus进行组件之间的数据传递,需要注意的有三点:

  1. 事件名必须保持统一;

  2. 派发数据的组件A内 emit 事件应在beforeDestory生命周期内;

  3. 接收数据的组件B内on事件记得要销毁。

;