Vue 组件有哪些通讯方式?这里有10种方式及示例代码
你好同学,我是沐爸,欢迎点赞、收藏、评论和关注。
Vue 的组件通讯方式到底有几种?还真说不好。不过最常用、最常见的是以下十种:
props、 e m i t 、 emit、 emit、parent/ c h i l d r e n 、 children、 children、refs、provide/inject、 a t t r s / attrs/ attrs/listeners、vuex/pinia、 eventBus/mitt、slot 作用域插槽、本地缓存
接下来一起看下各种通讯方式的特点、使用场景以及注意事项吧!Let’s go!
一、Props
- 概述:通过
props
属性,父组件可以向子组件传递数据。 - 特点:单向数据流,子组件不能直接修改
props
中的值(但可以通过触发事件请求父组件修改)。
父组件
<template><child-component :count="count"></child-component>
</template><script>import { ref } from "vue";import ChildComponent from "./ChildComponent.vue";const count = ref(100);
</script>
子组件
<template><div>{{ count }}</div>
</template><script setup>const props = defineProps(["count"]);console.log(props.count);
</script>
二、$emit
- 概述:子组件可以通过
$emit
触发事件,并传递数据给父组件。 - 特点:父组件需要监听这些事件来接收数据。
父组件
<template><child-component:count="count"@increment="count += $event"></child-component>
</template><script setup>import { ref } from "vue";import ChildComponent from "./ChildComponent.vue";const count = ref(100);
</script>
子组件
<template><div><div>{{ count }}</div><button @click="$emit('increment', 2)">点一下</button></div>
</template><script setup>const props = defineProps(["count"]);console.log(props.count);
</script>
注意,如果在子组件的 template
模板上使用 $emit
,请确保子组件只有一个根节点,否则会报警告。如果不想增加额外根节点,解决警告的另一种方式是使用 defineEmits
:
<template><div>{{ count }}</div><button @click="handleClick">Increment</button>
</template><script setup>const props = defineProps(["count"]);console.log(props.count);const emit = defineEmits(["increment"]);function handleClick() {emit("increment", 2);}
</script>
三、$parent / $children
- 概述:
$parent
用于访问组件的父实例;$children 用于访问当前实例的直接子组件(仅 Vue2 支持)。 - 特点:适用于简单的父子组件通信,但不建议在大型或复杂的项目中使用,因为它会增加组件间的耦合度。
- 使用:在子组件中,使用
this.$parent
访问父组件;在父组件中,使用this.$children
访问子组件(但注意$children 是一个数组,需要指定索引)。
父组件
<template><child-component :count="count" @increment="onIncrement"></child-component>
</template><script>import ChildComponent from "./ChildComponent.vue";export default {components: {ChildComponent,},data() {return {count: 100,};},methods: {onIncrement(e) {this.count += e;console.log(this.$children); // undefined},},};
</script>
子组件
<template><div><div>{{ count }}</div><button @click="handleClick">点一下</button></div>
</template><script>export default {props: ["count"],methods: {handleClick() {this.$parent.onIncrement(2);},},};
</script>
注意:
- 在选项式组件中,无论是在子组件的
template
模板上使用$emit
,还是在<script>
中使用$emit
,再或者使用this.$parent
访问父组件的属性和方法,请确保子组件只有一个根节点,否则会报警告。 $parent
只能用在选项式组件中,子组件使用this.$parent
访问父组件实例时,父组件也必须是选项式组件,请勿将选项式和组合式语法混用。
四、$refs
- 概述:通过
$refs
可以访问组件实例或 DOM 元素。 - 特点:主要用于父子组件之间的直接通信,但也可以用于访问任意组件实例。
选项式
父组件
<template><button @click="giveMoney">给儿子零花钱</button><child-component ref="child"></child-component>
</template><script>import ChildComponent from "./ChildComponent.vue";export default {components: {ChildComponent,},methods: {giveMoney() {this.$refs.child.changeMoney(1000);},},};
</script>
子组件
<template><div>零花钱:{{ money }} 元</div>
</template><script>export default {data() {return {money: 1,};},methods: {changeMoney(n) {this.money += n;},},};
</script>
组合式
父组件
<template><child-componentref="child":count="count"@increment="onIncrement"></child-component><button @click="giveMoney">给儿子零花钱</button>
</template><script setup>import { ref } from "vue";import ChildComponent from "./ChildComponent.vue";const child = ref(null);function giveMoney() {child.value.changeMoney(1000);}
</script>
子组件
<template><div>零花钱:{{ money }} 元</div>
</template><script setup>import { ref } from "vue";const money = ref(1);function changeMoney(n) {money.value += n;}// 子组件的数据和方法只有暴露出去,才能被外部访问到defineExpose({changeMoney,});
</script>
五、Provide / Inject
- 概述:
provide
和inject
允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深。 - 特点:跨层级组件通信,但要注意不要滥用,以免造成组件间的耦合度过高。
- 使用:在祖先组件中,使用
provide()
函数提供数据或方法;在子孙组件中,通过inject
选项接收这些数据或方法。
祖先组件
<template><div class="parent"><div>总资产:{{ money }} 元</div><child-component></child-component></div>
</template><script setup>import { provide, ref } from "vue";import ChildComponent from "./ChildComponent.vue";const money = ref(1000000000);function giveMoney(n) {money.value -= n;}provide("money", money);provide("giveMoney", giveMoney);
</script><style scoped>.parent {border: 1px solid red;padding: 10px;}
</style>
子组件
<template><div class="child"><div>儿子的零花钱:{{ MyMoney }} 元</div><button @click="handleClick">点一下</button><grand-child></grand-child></div>
</template><script setup>import GrandChild from "./GrandChild.vue";import { ref, inject } from "vue";const MyMoney = ref(1);const parentMoney = inject("money");const getMoney = inject("giveMoney");function handleClick() {if (parentMoney.value > 0) {MyMoney.value += 100;getMoney(100);}}
</script><style scoped>.child {border: 1px solid blue;padding: 10px;margin-top: 20px;}
</style>
孙组件
<template><div class="grand-child"><div>孙子的零花钱:{{ MyMoney }} 元</div><button @click="handleClick">点一下</button></div>
</template><script setup>import { ref, inject } from "vue";const MyMoney = ref(0);const parentMoney = inject("money");const getMoney = inject("giveMoney");function handleClick() {if (parentMoney.value > 0) {MyMoney.value += 1000;getMoney(1000);}}
</script><style scoped>.grand-child {border: 1px solid green;padding: 10px;margin-top: 20px;}
</style>
六、$attrs / $listeners
- $attrs 包含了组件所有透传 attributes。"透传 attribute"指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。
$listeners
包含了父作用域中的(不含.native 修饰符) v−on 事件监听器,可以通过v−on="listeners"
传入内部组件。$listeners
只能在 Vue2 中使用,Vue3 中的透传的事件包含在$attrs
中。
Vue3 语法
父组件
<template><div class="parent"><div>总资产:{{ money }} 元</div><child-component :money @click="giveMoney"></child-component></div>
</template><script setup>import { ref } from "vue";import ChildComponent from "./ChildComponent.vue";const money = ref(1000000000);function giveMoney(e) {if (e.target) {return;}money.value -= e;}
</script><style scoped>.parent {border: 1px solid red;padding: 10px;}
</style>
子组件
<template><div class="child"><div>子组件</div><grand-child v-bind="$attrs"></grand-child></div>
</template><script setup>import GrandChild from "./GrandChild.vue";
</script><style scoped>.child {border: 1px solid blue;padding: 10px;margin-top: 20px;}
</style>
孙组件
<template><div class="grand-child"><h2>孙组件</h2><div>祖先透传过来的数据: {{ $attrs }}</div><button @click="getMoney">花钱</button></div>
</template><script setup>import { useAttrs } from "vue";const attrs = useAttrs();console.log("孙组件", attrs);function getMoney() {attrs.onClick(1000);}
</script><style scoped>.grand-child {border: 1px solid green;padding: 10px;margin-top: 20px;}
</style>
giveMoney
可以作为一个方法而非事件进行传递:
<!-- 祖先组件 -->
<child-component :money :giveMoney></child-component><!-- 孙组件 -->
<button @click="getMoney">花钱</button>function getMoney() { attrs.giveMoney(1000) }
Vue2 语法
祖先组件
<template><div class="parent"><div>总资产:{{ money }} 元</div><child-component :money="money" @click="giveMoney"></child-component></div>
</template><script>import ChildComponent from "./ChildComponent.vue";export default {components: {ChildComponent,},data() {return {money: 1000000000,};},methods: {giveMoney(e) {if (e.target) {return;}this.money -= e;},},};
</script><style scoped>.parent {border: 1px solid red;padding: 10px;}
</style>
子组件
<template><div class="child"><div>子组件</div><grand-child v-bind="$attrs" v-on="$listeners"></grand-child></div>
</template><script>import GrandChild from "./GrandChild.vue";export default {components: {GrandChild,},mounted() {console.log("子组件", this.$attrs, this.$listeners);},};
</script><style scoped>.child {border: 1px solid blue;padding: 10px;margin-top: 20px;}
</style>
孙组件
<template><div class="grand-child"><h2>孙组件</h2><div>祖先透传过来的数据: {{ $attrs }}</div><button @click="getMoney">花钱</button></div>
</template><script>export default {methods: {getMoney() {this.$listeners.click(1000);},},mounted() {console.log("孙组件", this.$attrs, this.$listeners);},};
</script><style scoped>.grand-child {border: 1px solid green;padding: 10px;margin-top: 20px;}
</style>
运行效果同上面的 Vue3 代码。
七、slot 作用域插槽
- 概述:作用域插槽是插槽的一种特殊形式,它允许子组件将数据传递给插槽内容。这样,父组件就可以在插槽内容中访问子组件的数据,从而实现一种数据传递的效果。
- 特点:作用域插槽使得组件间的数据传递更加灵活和强大,但也需要更加复杂的实现和理解。
父组件
<template><div class="parent"><h3>父组件</h3><child-component><template #header="headerProps"><div>头部-来自作用域插槽的内容:{{ headerProps }}</div></template><template #default="defaultProps"><div>主体-来自作用域插槽的内容:{{ defaultProps }}</div></template><template #footer="footerProps"><div>底部-来自作用域插槽的内容:{{ footerProps }}</div></template></child-component></div>
</template><script setup>import ChildComponent from "./ChildComponent.vue";
</script><style scoped>.parent {border: 1px solid red;padding: 10px;}
</style>
子组件
<template><div class="child"><h3>子组件</h3><slot name="header" message="hello header"></slot><slot message="hello default"></slot><slot name="footer" message="hello footer"></slot></div>
</template><style scoped>.child {border: 1px solid blue;padding: 10px;margin-top: 20px;}
</style>
八、全局事件总线(EventBus / mitt)
EventBus
通过创建一个空的 Vue 实例作为全局事件总线,在组件间通过它来触发和监听事件。适用于任意组件间的通信,但需要注意事件名的冲突和事件清理。只有 Vue2 支持!mitt
是一个在 Vue.js 应用程序及其他 JavaScript 环境中使用的小型事件总线库。适用于当传统的 props、emits、provide/inject 等方式不足以满足需求的场景。
EventBus
event-bus.js
import Vue from "vue";export const EventBus = new Vue();
祖先组件
<template><div class="parent"><div>总资产:{{ money }} 元</div><button @click="handleClick">给孙子小明零花钱</button><child-component></child-component></div>
</template><script>import ChildComponent from "./ChildComponent.vue";import { EventBus } from "./event-bus";export default {components: {ChildComponent,},data() {return {money: 1000000000,};},methods: {handleClick() {this.money -= 1000;EventBus.$emit("give-money", 1000);},},};
</script><style scoped>.parent {border: 1px solid red;padding: 10px;}
</style>
子组件
<template><div class="child"><div>子组件</div><grand-child></grand-child><grand-child2></grand-child2></div>
</template><script>import GrandChild from "./GrandChild.vue";import GrandChild2 from "./GrandChild2.vue";export default {components: {GrandChild,GrandChild2,},};
</script><style scoped>.child {border: 1px solid blue;padding: 10px;margin-top: 20px;}
</style>
孙组件-小明
<template><div class="grand-child"><h2>孙组件-小明</h2><div>零花钱:{{ money }} 元</div><button @click="handleClick">给妹妹小红零花钱</button></div>
</template><script>import { EventBus } from "./event-bus";export default {data() {return {money: 0,};},methods: {handleClick() {if (this.money > 0) {this.money--;EventBus.$emit("give-sister", 1);}},},mounted() {EventBus.$on("give-money", (value) => {this.money += value;});},beforeDestroy() {EventBus.$off("give-money");},};
</script><style scoped>.grand-child {border: 1px solid green;padding: 10px;margin-top: 20px;}
</style>
孙组件-小红
<template><div class="grand-child"><h2>孙组件-小红</h2><div>零花钱:{{ money }} 元</div></div>
</template><script>import { EventBus } from "./event-bus";export default {data() {return {money: 0,};},mounted() {EventBus.$on("give-sister", (value) => {this.money += value;});},beforeDestroy() {EventBus.$off("give-sister");},};
</script><style scoped>.grand-child {border: 1px solid green;padding: 10px;margin-top: 20px;}
</style>
mitt
安装
npm install --save mitt
Emitter.js
import mitt from "mitt";export const Emitter = mitt();
祖先组件
<template><div class="parent"><div>总资产:{{ money }} 元</div><button @click="handleClick">给孙子小明零花钱</button><child-component></child-component></div>
</template><script setup>import ChildComponent from "./ChildComponent.vue";import { ref } from "vue";import { Emitter } from "./emitter";const money = ref(1000000000);function handleClick() {money.value -= 1000;Emitter.emit("give-money", 1000);}
</script><style scoped>.parent {border: 1px solid red;padding: 10px;}
</style>
子组件
<template><div class="child"><h3>子组件</h3><grand-child></grand-child><grand-child2></grand-child2></div>
</template><script setup>import GrandChild from "./GrandChild.vue";import GrandChild2 from "./GrandChild2.vue";
</script><style scoped>.child {border: 1px solid blue;padding: 10px;margin-top: 20px;}
</style>
孙组件-小明
<template><div class="grand-child"><h2>孙组件-小明</h2><div>零花钱:{{ money }} 元</div><button @click="handleClick">给妹妹小红零花钱</button></div>
</template><script setup>import { onBeforeUnmount, onMounted, ref } from "vue";import { Emitter } from "./emitter";const money = ref(0);function handleClick() {if (money.value > 0) {money.value--;Emitter.emit("give-sister", 1);}}onMounted(() => {Emitter.on("give-money", (value) => {money.value += value;});});onBeforeUnmount(() => {Emitter.off("give-money");});
</script><style scoped>.grand-child {border: 1px solid green;padding: 10px;margin-top: 20px;}
</style>
孙组件-小红
<template><div class="grand-child"><h2>孙组件-小红</h2><div>零花钱:{{ money }} 元</div></div>
</template><script>import { EventBus } from "./event-bus";export default {data() {return {money: 0,};},mounted() {EventBus.$on("give-sister", (value) => {this.money += value;});},beforeDestroy() {EventBus.$off("give-sister");},};
</script><style scoped>.grand-child {border: 1px solid green;padding: 10px;margin-top: 20px;}
</style>
效果同 EventBus。
九、Vuex / Pinia
- 概述:Vuex 和 Pinia 是 Vue.js 的状态管理模式和库,适用于大型应用,提供全局的状态存储。
Vuex
1.安装 Vuex
npm install vuex@next --save
2.配置 Vuex Store
import { createStore } from "vuex";export default createStore({state() {return {count: 0,};},getters: {doubleCount: (state) => state.count * 2,},mutations: {increment(state) {state.count++;},decrement(state) {state.count--;},},actions: {incrementIfOdd({ commit, state }) {if (state.count % 2 === 1) {commit("increment");}},},
});
3.在 Vue 应用中使用 Vuex Store
import { createApp } from "vue";
import App from "./App.vue";
import store from "./store";const app = createApp(App);app.use(store);
app.mount("#app");
4.在 Vue 组件中使用 Vuex Store
祖先组件
<template><div class="parent"><h3>祖先组件</h3><p>Count: {{ count }}</p><p>Double Count: {{ doubleCount }}</p><button @click="increment">Increment</button><button @click="decrement">Decrement</button><child-component></child-component></div>
</template><script setup>import ChildComponent from "./ChildComponent.vue";import { computed } from "vue";import { useStore } from "vuex";const store = useStore();const count = computed(() => store.state.count);const doubleCount = computed(() => store.getters.doubleCount);function increment() {store.commit("increment");}function decrement() {store.commit("decrement");}
</script><style scoped>.parent {border: 1px solid red;padding: 10px;}
</style>
子组件
<template><div class="child"><h3>子组件</h3><grand-child></grand-child><grand-child2></grand-child2></div>
</template><script setup>import GrandChild from "./GrandChild.vue";import GrandChild2 from "./GrandChild2.vue";
</script><style scoped>.child {border: 1px solid blue;padding: 10px;margin-top: 20px;}
</style>
孙组件-1、孙组件-2
<template><div class="grand-child"><h2>孙组件-1</h2><p>Count: {{ count }}</p><p>Double Count: {{ doubleCount }}</p><button @click="increment">Increment</button><button @click="decrement">Decrement</button></div>
</template><script setup>import { computed } from "vue";import { useStore } from "vuex";const store = useStore();const count = computed(() => store.state.count);const doubleCount = computed(() => store.getters.doubleCount);function increment() {store.commit("increment");}function decrement() {store.commit("decrement");}
</script><style scoped>.grand-child {border: 1px solid green;padding: 10px;margin-top: 20px;}
</style>
Pinia
1.安装 Pinia
npm install pinia --save
2.配置 Pinia store
// src/stores/counter.jsimport { defineStore } from "pinia";export const useCounterStore = defineStore("counter", {state: () => ({count: 0,}),getters: {doubleCount: (state) => state.count * 2,},actions: {increment() {this.count++;},decrement() {this.count--;},},
});
3.在 Vue 应用中使用 Pinia Store
// src/main.jsimport { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";const app = createApp(App);
const pinia = createPinia();app.use(pinia);
app.mount("#app");
4.在 Vue 组件中使用 Pinia Store
祖先组件
<template><div class="parent"><h3>祖先组件</h3><p>Count: {{ counterStore.count }}</p><p>Double Count: {{ counterStore.doubleCount }}</p><button @click="counterStore.increment">Increment</button><button @click="counterStore.decrement">Decrement</button><child-component></child-component></div>
</template><script setup>import ChildComponent from "./ChildComponent.vue";import { useCounterStore } from "@/stores/counter";const counterStore = useCounterStore();
</script><style scoped>.parent {border: 1px solid redpadding: 10px}
</style>
子组件
<template><div class="child"><h3>子组件</h3><grand-child></grand-child><grand-child2></grand-child2></div>
</template><script setup>import GrandChild from "./GrandChild.vue";import GrandChild2 from "./GrandChild2.vue";
</script><style scoped>.child {border: 1px solid bluepadding: 10pxmargin-top: 20px}
</style>
孙组件-1、孙组件-2
<template><div class="grand-child"><h2>孙组件</h2><p>Count: {{ counterStore.count }}</p><p>Double Count: {{ counterStore.doubleCount }}</p><button @click="counterStore.increment">Increment</button><button @click="counterStore.decrement">Decrement</button></div>
</template><script setup>import { useCounterStore } from "@/stores/counter";const counterStore = useCounterStore();
</script><style scoped>.grand-child {border: 1px solid greenpadding: 10pxmargin-top: 20px}
</style>
十、本地存储(LocalStorage/SessionStorage)
- 概述:利用浏览器的本地存储功能,实现跨会话或跨标签页的组件通信。
- 特点:适用于需要持久化数据的场景,但不适用于实时性要求较高的通信。同样也不适用于在同一页面多个组件共享数据的情形,因为某一组件更改了本地存储的数据,并不能响应式地更新相关组件的数据和视图。
好了,分享结束,谢谢点赞,下期再见。