你好同学,我是沐爸,欢迎点赞、收藏、评论和关注。
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-component
ref="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.js
import { 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.js
import { 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 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>孙组件</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 green
padding: 10px
margin-top: 20px
}
</style>
十、本地存储(LocalStorage/SessionStorage)
- 概述:利用浏览器的本地存储功能,实现跨会话或跨标签页的组件通信。
- 特点:适用于需要持久化数据的场景,但不适用于实时性要求较高的通信。同样也不适用于在同一页面多个组件共享数据的情形,因为某一组件更改了本地存储的数据,并不能响应式地更新相关组件的数据和视图。
好了,分享结束,谢谢点赞,下期再见。