vue中组件之间传值的八种方式
前言
在Vue开发中,组件之间的通信是一个常见的需求。本文将介绍几种常用的组件通信方式,包括props和$emit事件、provide/inject、自定义事件总线、Vuex状态管理、v-model、透传 Attributes、插槽 Slots。
Props 和 $emit(事件)
Props和$emit是Vue中最基本的组件通信方式。父组件通过props向子组件传递数据,子组件通过$emit触发事件向父组件传递数据。
1.Props传值:
首先子组件要显式声明它所接收的props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// ChildComponent.vue
<template>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
//此处是TypeScript的写法,使用类型标注来声明 props
const props = defineProps<{
message?: string;
}>();
//可以使用对象的形式来声明 props ,也可以使用字符串数组的形式
const props = defineProps(['message']);
</script>
|
然后父组件可以通过props向子组件传递数据:
1
2
3
4
5
6
7
8
|
// ParentComponent.vue
<template>
<ChildComponent message="Hello from Parent!" />
</template>
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue';
</script>
|
2.$emit传值:
子组件通过$emit触发事件向父组件传递数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// ChildComponent.vue
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script setup lang="ts">
import { defineEmits } from 'vue';
//定义子组件可以触发的事件及其参数类型
const emit = defineEmits<{
(event: 'message', payload: string): void;
}>();
//触发事件并传递数据
function sendMessage() {
emit('message', 'Hello from Child!');
}
</script>
|
父组件监听子组件触发的事件并接收数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// ParentComponent.vue
<template>
<ChildComponent @message="handleMessage" />
</template>
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue';
function handleMessage(payload: string) {
console.log('Received message from child:', payload);
}
//控制台打印:Received message from child: Hello from Child!
</script>
|
Provide/Inject
Provide/Inject 是 Vue 3 中提供的一种跨级组件通信方式,允许祖先组件向后代组件传递数据,而不需要通过中间组件逐层传递,父组件可以通过provide选项提供数据,子组件(包括跨层级的子孙组件)可以通过inject选项注入这些数据,这样也可以避免props的逐层传递。
1. Provide 提供数据:
1
2
3
4
5
6
7
8
9
10
11
12
|
// GrandParentComponent.vue
<template>
<ParentComponent />
</template>
<script setup lang="ts">
import ParentComponent from './ParentComponent.vue';
import { provide } from 'vue';
provide('message', 'Hello from Grandparent!');
</script>
|
也可在整个应用层面提供依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// App.vue
<template>
</template>
<script setup lang="ts">
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
|
2. Inject 注入数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// ChildComponent.vue
<template>
<div>{{ message }}</div>//显示Hello from Grandparent!
</template>
<script setup lang="ts">
import { inject } from 'vue';
const message = inject('message');
//如果有多个父组件提供了相同键的数据,注入将解析为组件链上最近的父组件所注入的值
</script>
|
Provide和Inject需要一起使用,适用于跨多层组件传递数据的场景,避免了props的逐层传递,需要注意的是provide和inject绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
自定义事件总线
自定义事件总线是一种轻量级的组件通信方式,适用于非父子关系的组件之间的通信。可以使用一个空的Vue实例作为事件总线,通过$emit和$on方法实现组件间的通信。
1. 创建事件总线:
1
2
3
|
// eventBus.ts
import { createApp } from 'vue';
export const eventBus = createApp({});
|
2. 组件间通信:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// ComponentA.vue
<template>
<button @click="sendMessage">Send Message to B</button>
</template>
<script setup lang="ts">
import { eventBus } from './eventBus';
function sendMessage() {
eventBus.emit('message', 'Hello from A!');
}
</script>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// ComponentB.vue
<template>
<div>{{ message }}</div>//显示Hello from A!
</template>
<script setup lang="ts">
import { eventBus } from './eventBus';
const message = ref('');
eventBus.on('message', (payload: string) => {
message.value = payload;
});
</script>
|
Vuex 状态管理
Vuex 是 Vue.js 官方提供的状态管理库,适用于大型应用中复杂的组件通信需求。通过集中管理应用的状态,可以实现跨组件的数据共享和状态同步。
1. 安装 Vuex:
1
2
|
npm install vuex@next
|
2. 创建 Vuex Store:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// store/index.ts
import { createStore } from 'vuex';
const store = createStore({
state() {
return {
message: 'Hello from Vuex!'
};
},
mutations: {
setMessage(state, payload) {
state.message = payload;
}
}
});
export default store;
|
3. 在组件中使用 Vuex:
1
2
3
4
5
6
7
8
9
10
11
|
// Component.vue
<template>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
import { useStore } from 'vuex';
const store = useStore();
const message = computed(() => store.state.message);
</script>
|
V-Model 双向绑定
v-model 是 Vue 提供的双向数据绑定语法糖,适用于父子组件之间的数据同步。通过在子组件中使用modelValue和update:modelValue事件,可以实现父组件和子组件之间的双向绑定。
1. 子组件使用 v-model:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// ChildComponent.vue
<template>
<div>Parent bound v-model is: {{ model }}</div>
//这里的`model`会随着父组件的变化而变化,实现了双向绑定。
<button @click="update">Increment</button>
</template>
<script setup>
//从 Vue3.4开始,推荐的实现方式是使用 defineModel()宏
const model = defineModel()
function update() {
model.value++
}
</script>
|
2. 父组件使用 v-model:
1
2
3
4
5
6
7
8
9
10
11
12
|
// ParentComponent.vue
<template>
<ChildComponent v-model="parentValue" />
<div>Parent value is: {{ parentValue }}</div>
//这里的`parentValue`会随着子组件的更新而变化,实现了双向绑定。
</template>
<script setup lang="ts">
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const parentValue = ref(0);
</script>
|
defineModel需要 Vue 3.4 及以上版本支持,它的底层机制是通过props和$emit事件实现的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// ChildComponent.vue
<template>
<input :value="modelValue" @input="event => emit('update:modelValue', event.target.value)" />
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: number;
}>();
const emit = defineEmits<{
(event: 'update:modelValue', payload: number): void;
//ts类型标注,声明子组件可以触发的事件及其参数类型
}>();
</script>
|
然后在父组件中v-model = "parentValue"将会被编译成:
1
2
3
4
5
|
// ParentComponent.vue
<template>
<ChildComponent :modelValue="parentValue" @update:modelValue="$event => (parentValue = $event)" />
</template>
|
透传 Attributes
透传 attributes 是指传递给子组件,却没有在子组件中显式声明为 props 或 emits 的 attribute或 v-on事件监视器,最常见的是class、style和id。这些属性会被自动添加到子组件的根元素上,适用于需要传递大量属性但不想逐一声明的场景。
1. 透传 Attributes 示例:
1
2
3
4
5
6
7
8
9
10
|
// ChildComponent.vue
<template>
<div>Child Component</div>
//如果已经有了class、style或id属性,这些属性会和从父组件继承的值合并
</template>
<script setup lang="ts">
//子组件没有显式声明任何 props 或 emits
</script>
|
1
2
3
4
5
6
7
8
9
|
// ParentComponent.vue
<template>
<ChildComponent class="custom-class" style="color: red;" id="child-component" />
//父组件传递的 class、style 和 id 会被自动添加到子组件的根元素上
</template>
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue';
</script>
|
最后渲染的结果:
1
2
3
4
|
<div id="child-component" class="custom-class" style="color: red;">
Child Component
</div>
|
- 禁用透传 Attributes
有时我们可能不希望某些属性被透传到子组件,这时可以使用 inheritAttrs: false 选项来禁用透传。
1
2
3
4
5
6
7
8
9
10
|
// ChildComponent.vue
<template>
<div>Child Component</div>
</template>
<script setup lang="ts">
defineOptions({
inheritAttrs: false
});
</script>
|
当子组件有多个根元素时,Vue 默认会将透传的属性添加到第一个根元素上,这可能不是我们想要的效果。通过禁用透传,我们可以手动将属性绑定到特定的元素上。
1
2
3
4
5
6
7
8
9
10
11
12
|
// ChildComponent.vue
<template>
<div>First Root Element</div>
<div v-bind="$attrs">Second Root Element with inherited attributes</div>
//将透传的属性绑定到第二个根元素上
</template>
<script setup lang="ts">
defineOptions({
inheritAttrs: false
});
</script>
|
注意:没有参数的v-bind会将一个对象的所有属性都作为attribute应用到目标元素上。
- 多根节点的透传
多根节点的组件没有自动attribute透传功能,如果$attrs没有被显式绑定,将会抛出一个运行时的警告。
1
2
3
4
5
6
7
8
|
// MultiRootComponent.vue
<template>
<header>First Root Element</header>
<footer>Second Root Element</footer>
</template>
<script setup lang="ts">
//没有显式绑定 $attrs,会抛出警告
</script>
|
要解决这个问题,可以显式绑定$attrs到其中一个根元素上:
1
2
3
4
5
6
7
|
// MultiRootComponent.vue
<template>
<header v-bind="$attrs">First Root Element with inherited attributes</header>
<footer>Second Root Element</footer>
</template>
<script setup lang="ts">
</script>
|
插槽 Slots
插槽(Slots)是Vue中用于组件间传递内容的一种机制,允许父组件向子组件传递任意的模板内容。插槽适用于需要在子组件中动态渲染父组件提供的内容的场景。
1. 基本插槽
1
2
3
4
5
6
7
8
9
|
// ChildComponent.vue
<template>
<div>
<slot></slot> //插槽占位符
</div>
</template>
<script setup lang="ts">
</script>
|
1
2
3
4
5
6
7
8
9
10
11
|
// ParentComponent.vue
<template>
<ChildComponent>
<p>This is content passed from Parent to Child via slot.</p>
//传递给子组件的内容,会渲染在子组件的插槽位置
</ChildComponent>
</template>
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue';
</script>
|
- 具名插槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// ChildComponent.vue
<template>
<div>
<header>
<slot name="header"></slot> //具名插槽
</header>
<main>
<slot></slot> //默认插槽
</main>
<footer>
<slot name="footer"></slot> //具名插槽
</footer>
</div>
</template>
<script setup lang="ts">
</script>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// ParentComponent.vue
<template>
<ChildComponent>
<template #header>//使用含v-slot的<template>元素,并将插槽的名字传给该指令,此处是语法糖,等同于v-slot:header
<h1>This is the header content.</h1>
</template>
<p>This is the main content.</p>
<template #footer>
<p>This is the footer content.</p>
</template>
</ChildComponent>
</template>
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue';
</script>
|
- 条件插槽
根据内容是否被传入了插槽来渲染某些内容,可以使用$slots和v-if来实现条件插槽:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// ChildComponent.vue
<template>
<div>
<header v-if="$slots.header">
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer v-if="$slots.footer">
<slot name="footer"></slot>
</footer>
</div>
</template>
<script setup lang="ts">
</script>
|
- 动态插槽
1
2
3
4
5
6
7
8
9
10
11
|
// ParentComponent.vue
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
|
在上面的例子中,dynamicSlotName是一个动态变量,可以根据需要传递不同的插槽名称,实现更灵活的插槽内容传递。
- 作用域插槽
作用域插槽(Scoped Slots)是指插槽可以接收来自子组件的数据,并将这些数据传递给插槽的内容。通过作用域插槽,父组件可以更灵活地控制插槽的渲染内容。
1
2
3
4
5
6
7
8
9
|
// ChildComponent.vue
<template>
<div>
<slot :data="slotData"></slot> //将数据传递给插槽
</div>
</template>
<script setup lang="ts">
const slotData = { message: 'Hello from ChildComponent!' };
</script>
|
1
2
3
4
5
6
7
8
9
10
11
|
// ParentComponent.vue
<template>
<ChildComponent>
<template #default="{ data }">
<p>{{ data.message }}</p>
</template>
</ChildComponent>
</template>
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue';
</script>
|
在上面的例子中,子组件通过slot将slotData对象传递给插槽内容,父组件通过作用域插槽接收这个数据并进行渲染。
注意:在 Vue 3 中,作用域插槽的语法有所简化,可以直接在template标签上使用#default来定义默认插槽(最好使用显示的默认插槽防止歧义,即使用<template #default>),并通过解构语法获取传递的数据。
结论
本文介绍了八种常用的Vue组件通信方式,根据具体的应用场景和需求,选择合适的通信方式可以提高代码的可维护性和可读性。Vue组件传参的方式多种多样,本文只介绍了几种常见的方法,实际可以有更多的变通和组合使用,灵活运用这些技术可以帮助开发者更好地管理组件之间的通信。
参考资料