MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue中组件通信的多种方式对比

2023-09-173.0k 阅读

父子组件通信

props 传递数据

在 Vue 中,父组件向子组件传递数据最常用的方式就是通过 props。这种方式非常直观,父组件将数据作为属性传递给子组件,子组件通过声明 props 来接收这些数据。

首先,创建一个父组件 Parent.vue

<template>
  <div>
    <child-component :message="parentMessage"></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentMessage: '这是来自父组件的数据'
    };
  }
};
</script>

然后是子组件 ChildComponent.vue

<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  props: ['message']
};
</script>

在这个例子中,父组件 Parent.vueparentMessage 数据通过 :message 传递给子组件 ChildComponent.vue,子组件通过声明 props 中的 message 来接收并展示。

从本质上来说,props 是一种单向数据流,父组件可以改变传递给子组件的数据,而子组件不能直接修改父组件传递过来的 props。这是为了防止子组件无意中修改了父组件的状态,导致数据流向难以追踪。如果子组件需要对接收到的数据进行修改,通常的做法是在子组件中定义一个本地数据,初始值设为接收到的 props,然后对本地数据进行操作。

$emit 触发事件

子组件向父组件传递数据或通知父组件某些事情发生,通常使用 $emit 方法。子组件通过触发自定义事件,父组件监听这些事件并执行相应的回调函数。

继续以上面的父子组件为例,修改子组件 ChildComponent.vue 来传递数据:

<template>
  <div>
    <button @click="sendDataToParent">发送数据给父组件</button>
  </div>
</template>

<script>
export default {
  methods: {
    sendDataToParent() {
      this.$emit('child-event', '这是子组件发送的数据');
    }
  }
};
</script>

同时修改父组件 Parent.vue 来监听事件:

<template>
  <div>
    <child-component @child-event="handleChildEvent"></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  methods: {
    handleChildEvent(data) {
      console.log('接收到子组件的数据:', data);
    }
  }
};
</script>

当子组件的按钮被点击时,sendDataToParent 方法通过 $emit 触发 child - event 事件,并传递数据。父组件通过 @child - event 监听该事件,并在回调函数 handleChildEvent 中处理接收到的数据。

这种方式本质上是基于事件驱动的机制,子组件触发事件,父组件响应。它使得子组件与父组件之间的通信更加灵活,子组件不需要知道父组件具体如何处理数据,只负责触发事件和传递数据。

非父子组件通信

EventBus(事件总线)

当组件之间没有直接的父子关系,但需要进行通信时,可以使用 EventBus。EventBus 本质上是一个 Vue 实例,通过它来触发和监听事件,实现组件之间的通信。

首先创建一个 eventBus.js 文件:

import Vue from 'vue';
export const eventBus = new Vue();

然后假设有两个非父子组件 ComponentA.vueComponentB.vue。在 ComponentA.vue 中触发事件:

<template>
  <div>
    <button @click="sendMessage">发送消息</button>
  </div>
</template>

<script>
import { eventBus } from './eventBus.js';

export default {
  methods: {
    sendMessage() {
      eventBus.$emit('message - event', '这是来自 ComponentA 的消息');
    }
  }
};
</script>

ComponentB.vue 中监听事件:

<template>
  <div>
    <p>等待接收消息...</p>
  </div>
</template>

<script>
import { eventBus } from './eventBus.js';

export default {
  created() {
    eventBus.$on('message - event', (data) => {
      console.log('接收到消息:', data);
    });
  }
};
</script>

ComponentA 中的按钮被点击时,通过 eventBus 触发 message - event 事件,ComponentBcreated 钩子函数中通过 eventBus 监听该事件并处理数据。

EventBus 的优点是简单直接,适用于组件之间关系较为松散的通信场景。然而,随着应用规模的增大,当有大量组件使用 EventBus 进行通信时,事件的管理和维护会变得困难,因为很难追踪事件的触发和监听位置。

Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

使用 Vuex 时,首先要安装它:

npm install vuex --save

然后创建一个 store.js 文件来配置 Vuex:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    sharedData: '初始共享数据'
  },
  mutations: {
    updateSharedData(state, newData) {
      state.sharedData = newData;
    }
  },
  actions: {
    updateSharedDataAction({ commit }, newData) {
      commit('updateSharedData', newData);
    }
  }
});

export default store;

main.js 中引入并挂载 Vuex:

import Vue from 'vue';
import App from './App.vue';
import store from './store';

Vue.config.productionTip = false;

new Vue({
  store,
  render: h => h(App)
}).$mount('#app');

假设有两个组件 ComponentX.vueComponentY.vue 都需要访问和修改共享数据。在 ComponentX.vue 中修改数据:

<template>
  <div>
    <button @click="updateData">更新数据</button>
  </div>
</template>

<script>
export default {
  methods: {
    updateData() {
      this.$store.dispatch('updateSharedDataAction', '新的共享数据');
    }
  }
};
</script>

ComponentY.vue 中获取数据:

<template>
  <div>
    <p>{{ sharedData }}</p>
  </div>
</template>

<script>
export default {
  computed: {
    sharedData() {
      return this.$store.state.sharedData;
    }
  }
};
</script>

Vuex 通过 state 来存储共享状态,mutations 来修改状态,actions 来处理异步操作并提交 mutations。所有组件都可以通过 $store 来访问和修改状态。

与 EventBus 相比,Vuex 更适合大型应用中复杂状态的管理,它有明确的状态管理规则和结构,便于维护和调试。但对于小型应用,引入 Vuex 可能会增加项目的复杂性。

provide 和 inject

provideinject 是 Vue 提供的一对选项,用于实现祖先组件向其所有子孙组件注入数据,而不需要在组件链中逐个传递 props

假设有一个祖先组件 Grandparent.vue

<template>
  <div>
    <child - component></child - component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  provide() {
    return {
      sharedValue: '这是共享的值'
    };
  }
};
</script>

然后在子孙组件 ChildComponent.vue 中接收数据:

<template>
  <div>
    <p>{{ sharedValue }}</p>
  </div>
</template>

<script>
export default {
  inject: ['sharedValue']
};
</script>

provide 选项是一个对象或返回一个对象的函数,这个对象包含要提供给子孙组件的数据。inject 选项是一个数组或对象,用于接收祖先组件提供的数据。

从本质上讲,provideinject 建立了一种依赖注入机制,使得子孙组件可以直接获取祖先组件提供的数据,而不需要通过层层传递 props。然而,这种方式的数据传递是单向的,子孙组件不能直接修改通过 inject 接收到的数据。如果需要修改,通常需要通过事件等方式通知祖先组件来进行修改。

兄弟组件通信

通过共同父组件

兄弟组件之间通信可以通过它们的共同父组件来实现。例如有两个兄弟组件 BrotherA.vueBrotherB.vue,它们的父组件是 Parent.vue

BrotherA.vue 向父组件传递数据:

<template>
  <div>
    <button @click="sendDataToParent">发送数据给父组件</button>
  </div>
</template>

<script>
export default {
  methods: {
    sendDataToParent() {
      this.$emit('brother - a - event', '这是 BrotherA 发送的数据');
    }
  }
};
</script>

Parent.vue 接收 BrotherA 传递的数据,并将其传递给 BrotherB

<template>
  <div>
    <brother - a @brother - a - event="handleBrotherAEvent"></brother - a>
    <brother - b :message="brotherAMessage"></brother - b>
  </div>
</template>

<script>
import BrotherA from './BrotherA.vue';
import BrotherB from './BrotherB.vue';

export default {
  components: {
    BrotherA,
    BrotherB
  },
  data() {
    return {
      brotherAMessage: ''
    };
  },
  methods: {
    handleBrotherAEvent(data) {
      this.brotherAMessage = data;
    }
  }
};
</script>

BrotherB.vue 接收父组件传递的数据:

<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  props: ['message']
};
</script>

这种方式实际上是将兄弟组件之间的通信转化为父子组件之间的通信,通过父组件作为桥梁来传递数据。它的优点是逻辑比较清晰,易于理解和维护,适用于兄弟组件之间数据交互不太频繁的场景。

使用 EventBus

兄弟组件之间也可以使用 EventBus 进行通信,和非父子组件使用 EventBus 通信的方式类似。

假设 BrotherC.vueBrotherD.vue 是两个兄弟组件,在 BrotherC.vue 中触发事件:

<template>
  <div>
    <button @click="sendMessage">发送消息</button>
  </div>
</template>

<script>
import { eventBus } from './eventBus.js';

export default {
  methods: {
    sendMessage() {
      eventBus.$emit('brother - message - event', '这是来自 BrotherC 的消息');
    }
  }
};
</script>

BrotherD.vue 中监听事件:

<template>
  <div>
    <p>等待接收消息...</p>
  </div>
</template>

<script>
import { eventBus } from './eventBus.js';

export default {
  created() {
    eventBus.$on('brother - message - event', (data) => {
      console.log('接收到消息:', data);
    });
  }
};
</script>

使用 EventBus 进行兄弟组件通信,使得兄弟组件之间的耦合度降低,它们不需要通过父组件来间接传递数据。但同样,当项目规模变大时,EventBus 管理事件会变得复杂,可能会出现事件命名冲突等问题。

组件库内组件通信

基于插槽和作用域插槽

在开发组件库时,经常会用到插槽和作用域插槽来实现组件之间的通信和定制。例如,有一个 Dialog 组件,它可以接收不同的内容并展示。

Dialog.vue

<template>
  <div class="dialog">
    <slot></slot>
  </div>
</template>

<style scoped>
.dialog {
  border: 1px solid #ccc;
  padding: 10px;
}
</style>

使用 Dialog 组件的父组件:

<template>
  <div>
    <dialog>
      <p>这是对话框的内容</p>
    </dialog>
  </div>
</template>

<script>
import Dialog from './Dialog.vue';

export default {
  components: {
    Dialog
  }
};
</script>

这里通过默认插槽,父组件可以向 Dialog 组件传递内容。而作用域插槽则可以让子组件向父组件传递数据,同时父组件可以根据这些数据来定制展示。

假设 List.vue 是一个列表组件,它通过作用域插槽向父组件传递列表项数据:

<template>
  <div>
    <ul>
      <li v - for="(item, index) in listData" :key="index">
        <slot :item="item"></slot>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      listData: ['苹果', '香蕉', '橙子']
    };
  }
};
</script>

父组件使用 List 组件并通过作用域插槽定制展示:

<template>
  <div>
    <list>
      <template v - slot:default="slotProps">
        <span>{{ slotProps.item }} - 自定义展示</span>
      </template>
    </list>
  </div>
</template>

<script>
import List from './List.vue';

export default {
  components: {
    List
  }
};
</script>

插槽和作用域插槽在组件库开发中是非常强大的工具,它们允许组件之间进行灵活的内容传递和定制,同时保持组件的复用性。

组件库内部的事件系统

一些组件库会构建自己的内部事件系统来实现组件之间的通信。以 Element - UI 为例,其组件之间可能会通过内部定义的事件来进行交互。

例如,ElSelect 组件在选择值发生变化时会触发 change 事件,父组件可以监听这个事件来获取选择的值并进行相应处理:

<template>
  <div>
    <el - select v - model="selectedValue" @change="handleSelectChange">
      <el - option v - for="item in options" :key="item.value" :label="item.label" :value="item.value"></el - option>
    </el - select>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selectedValue: '',
      options: [
        { value: 'option1', label: '选项1' },
        { value: 'option2', label: '选项2' }
      ]
    };
  },
  methods: {
    handleSelectChange(value) {
      console.log('选择的值发生变化:', value);
    }
  }
};
</script>

组件库内部的事件系统通常是基于 Vue 的事件机制进行封装和扩展的,它为组件之间的交互提供了一种标准化的方式,使得开发者在使用组件库时能够方便地实现组件间的通信和联动。

动态组件通信

动态组件切换时的通信

在 Vue 中,使用 component 标签结合 is 属性可以实现动态组件切换。在动态组件切换过程中,也可能需要进行组件通信。

假设有两个动态组件 DynamicComponentA.vueDynamicComponentB.vue,以及一个父组件 DynamicParent.vue

DynamicComponentA.vue

<template>
  <div>
    <p>这是 DynamicComponentA</p>
    <button @click="sendDataToParent">发送数据给父组件</button>
  </div>
</template>

<script>
export default {
  methods: {
    sendDataToParent() {
      this.$emit('a - to - parent - event', '这是来自 A 的数据');
    }
  }
};
</script>

DynamicComponentB.vue

<template>
  <div>
    <p>这是 DynamicComponentB</p>
    <button @click="sendDataToParent">发送数据给父组件</button>
  </div>
</template>

<script>
export default {
  methods: {
    sendDataToParent() {
      this.$emit('b - to - parent - event', '这是来自 B 的数据');
    }
  }
};
</script>

DynamicParent.vue

<template>
  <div>
    <button @click="switchComponent">切换组件</button>
    <component :is="currentComponent" @a - to - parent - event="handleAEvent" @b - to - parent - event="handleBEvent"></component>
  </div>
</template>

<script>
import DynamicComponentA from './DynamicComponentA.vue';
import DynamicComponentB from './DynamicComponentB.vue';

export default {
  components: {
    DynamicComponentA,
    DynamicComponentB
  },
  data() {
    return {
      currentComponent: 'DynamicComponentA'
    };
  },
  methods: {
    switchComponent() {
      this.currentComponent = this.currentComponent === 'DynamicComponentA'? 'DynamicComponentB' : 'DynamicComponentA';
    },
    handleAEvent(data) {
      console.log('接收到 A 组件的数据:', data);
    },
    handleBEvent(data) {
      console.log('接收到 B 组件的数据:', data);
    }
  }
};
</script>

在这个例子中,父组件通过监听不同动态组件触发的事件来实现通信。当动态组件切换时,父组件仍然能够正确接收来自不同组件的事件和数据。

动态组件间共享数据

有时候,动态切换的组件之间可能需要共享数据。可以使用 Vuex 来实现这一点,因为 Vuex 的状态是全局共享的,无论哪个动态组件都可以访问和修改。

以之前的 DynamicComponentA.vueDynamicComponentB.vue 为例,假设它们都需要访问和修改一个共享的计数器。

store.js 中配置 Vuex:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    counter: 0
  },
  mutations: {
    increment(state) {
      state.counter++;
    }
  },
  actions: {
    incrementAction({ commit }) {
      commit('increment');
    }
  }
});

export default store;

DynamicComponentA.vue 中增加计数器:

<template>
  <div>
    <p>计数器: {{ counter }}</p>
    <button @click="incrementCounter">增加计数器</button>
  </div>
</template>

<script>
export default {
  computed: {
    counter() {
      return this.$store.state.counter;
    }
  },
  methods: {
    incrementCounter() {
      this.$store.dispatch('incrementAction');
    }
  }
};
</script>

DynamicComponentB.vue 中也可以获取和修改这个计数器:

<template>
  <div>
    <p>计数器: {{ counter }}</p>
    <button @click="incrementCounter">增加计数器</button>
  </div>
</template>

<script>
export default {
  computed: {
    counter() {
      return this.$store.state.counter;
    }
  },
  methods: {
    incrementCounter() {
      this.$store.dispatch('incrementAction');
    }
  }
};
</script>

通过 Vuex,动态组件之间可以方便地共享和修改数据,而不需要通过复杂的组件通信方式来传递数据。

跨级组件通信

多级父子组件间直接通信

在一些情况下,可能会有多级父子组件的结构,并且希望跳过中间层级,实现跨级组件之间的直接通信。虽然 Vue 官方不推荐深度嵌套的组件结构,但在某些特定场景下还是会遇到。

假设有一个三级父子组件结构,Grandparent.vue -> Parent.vue -> Child.vueGrandparent.vue 希望直接向 Child.vue 传递数据。

一种方法是通过 props 层层传递,Grandparent.vue 传递给 Parent.vueParent.vue 再传递给 Child.vueGrandparent.vue

<template>
  <div>
    <parent - component :message="grandparentMessage"></parent - component>
  </div>
</template>

<script>
import ParentComponent from './ParentComponent.vue';

export default {
  components: {
    ParentComponent
  },
  data() {
    return {
      grandparentMessage: '这是来自祖父组件的数据'
    };
  }
};
</script>

ParentComponent.vue

<template>
  <div>
    <child - component :message="parentMessage"></child - component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  props: ['message'],
  data() {
    return {
      parentMessage: this.message
    };
  }
};
</script>

ChildComponent.vue

<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  props: ['message']
};
</script>

这种方式虽然可以实现跨级传递,但如果层级较多,代码会变得繁琐,并且中间层级的组件可能并不关心这个数据,只是起到传递的作用。

使用 provide 和 inject 跨级通信

为了简化跨级组件通信,可以使用 provideinject。还是以上面的三级父子组件为例,在 Grandparent.vue 中提供数据:

<template>
  <div>
    <parent - component></parent - component>
  </div>
</template>

<script>
import ParentComponent from './ParentComponent.vue';

export default {
  components: {
    ParentComponent
  },
  provide() {
    return {
      crossLevelData: '这是跨级传递的数据'
    };
  }
};
</script>

ChildComponent.vue 中接收数据:

<template>
  <div>
    <p>{{ crossLevelData }}</p>
  </div>
</template>

<script>
export default {
  inject: ['crossLevelData']
};
</script>

通过 provideinject,可以直接在祖先组件和子孙组件之间建立数据传递关系,跳过中间层级的组件。但要注意,这种方式的数据传递是单向的,并且可能会使组件之间的依赖关系变得不那么直观,所以在使用时需要谨慎考虑。

Vuex 实现跨级通信

Vuex 也可以用于跨级组件通信。由于 Vuex 的状态是全局的,任何组件都可以访问和修改。

假设在 Grandparent.vue 中修改 Vuex 的状态:

<template>
  <div>
    <button @click="updateVuexData">更新 Vuex 数据</button>
    <parent - component></parent - component>
  </div>
</template>

<script>
import ParentComponent from './ParentComponent.vue';

export default {
  components: {
    ParentComponent
  },
  methods: {
    updateVuexData() {
      this.$store.dispatch('updateCrossLevelData', '新的跨级数据');
    }
  }
};
</script>

store.js 中定义 updateCrossLevelData 相关的 actionmutation

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    crossLevelData: '初始跨级数据'
  },
  mutations: {
    updateCrossLevelData(state, newData) {
      state.crossLevelData = newData;
    }
  },
  actions: {
    updateCrossLevelDataAction({ commit }, newData) {
      commit('updateCrossLevelData', newData);
    }
  }
});

export default store;

ChildComponent.vue 中获取 Vuex 的数据:

<template>
  <div>
    <p>{{ crossLevelData }}</p>
  </div>
</template>

<script>
export default {
  computed: {
    crossLevelData() {
      return this.$store.state.crossLevelData;
    }
  }
};
</script>

使用 Vuex 进行跨级通信,使得组件之间的通信更加统一和可维护,特别是在应用规模较大,组件层级较深的情况下,优势更加明显。但同样要注意 Vuex 状态管理的复杂性,合理划分状态和操作。

总结各种通信方式的适用场景

  1. 父子组件通信
    • props 传递数据:适用于父组件向子组件传递只读数据,例如展示的数据、配置信息等。只要子组件不需要修改传递过来的数据,使用 props 是非常简洁和直观的方式。
    • $emit 触发事件:当子组件需要向父组件反馈信息,如用户操作结果、状态变化等,使用 $emit 触发事件并传递数据给父组件是很好的选择。
  2. 非父子组件通信
    • EventBus:适合小型应用或组件之间关系较为松散的通信场景,例如在一些简单的单页应用中,几个不相关的组件之间偶尔需要进行数据交互。但对于大型应用,由于事件管理的复杂性,不建议大量使用。
    • Vuex:适用于大型应用中复杂状态的管理,多个组件需要共享和同步状态,并且状态变化需要可预测和易于调试的场景。例如电商应用中的购物车、用户登录状态等功能,使用 Vuex 可以更好地管理状态。
    • provide 和 inject:适用于祖先组件向子孙组件传递数据,特别是在组件层级较深,且中间层级组件不需要处理该数据的情况下。例如,一些全局配置信息、主题设置等可以通过这种方式传递。
  3. 兄弟组件通信
    • 通过共同父组件:适用于兄弟组件之间数据交互不太频繁,且逻辑较为简单的场景。通过父组件作为桥梁传递数据,逻辑清晰,易于理解和维护。
    • 使用 EventBus:当兄弟组件之间需要更灵活的通信,且不希望通过父组件间接传递数据时,可以使用 EventBus。但要注意事件命名冲突和事件管理的问题。
  4. 组件库内组件通信
    • 基于插槽和作用域插槽:在组件库开发中,当需要组件具有高度的可定制性,允许使用者传入不同的内容或根据子组件传递的数据定制展示时,插槽和作用域插槽是非常强大的工具。例如,列表组件根据不同的业务需求定制列表项的展示。
    • 组件库内部的事件系统:为组件库提供了一种标准化的组件间交互方式,方便开发者在使用组件库时实现组件间的通信和联动。例如,表单组件之间的联动,如选择框选择后触发输入框的变化等。
  5. 动态组件通信
    • 动态组件切换时的通信:当动态切换组件时,通过父组件监听不同动态组件触发的事件来实现通信,适用于动态组件与父组件之间的交互场景,如动态加载的表单组件提交数据给父组件。
    • 动态组件间共享数据:使用 Vuex 可以方便地实现动态组件之间共享和修改数据,特别是在动态组件需要共享一些全局状态,如计数器、用户权限等情况下。
  6. 跨级组件通信
    • 多级父子组件间直接通信(props 层层传递):适用于层级不是特别深,且对组件结构和数据传递过程要求较为清晰的场景。虽然代码可能会繁琐一些,但数据流向明确。
    • 使用 provide 和 inject 跨级通信:适用于组件层级较深,且中间层级组件不需要处理传递数据的情况,能够简化数据传递过程。但要注意数据单向性和依赖关系不直观的问题。
    • Vuex 实现跨级通信:在大型应用中,当跨级组件之间需要共享和同步状态,且希望有统一的状态管理方式时,Vuex 是一个很好的选择。它可以使跨级通信更加可维护和可预测。

在实际项目中,需要根据具体的业务需求、应用规模和组件之间的关系,合理选择合适的组件通信方式,以提高代码的可维护性、可扩展性和性能。