vue中组件之间传值的八种方式

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>

ProvideInject需要一起使用,适用于跨多层组件传递数据的场景,避免了props的逐层传递,需要注意的是provideinject绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

自定义事件总线

自定义事件总线是一种轻量级的组件通信方式,适用于非父子关系的组件之间的通信。可以使用一个空的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 提供的双向数据绑定语法糖,适用于父子组件之间的数据同步。通过在子组件中使用modelValueupdate: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 是指传递给子组件,却没有在子组件中显式声明为 propsemits 的 attribute或 v-on事件监视器,最常见的是classstyleid。这些属性会被自动添加到子组件的根元素上,适用于需要传递大量属性但不想逐一声明的场景。

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>
  1. 禁用透传 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应用到目标元素上。

  1. 多根节点的透传

多根节点的组件没有自动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. 具名插槽
 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>
  1. 条件插槽

根据内容是否被传入了插槽来渲染某些内容,可以使用$slotsv-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. 动态插槽
 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是一个动态变量,可以根据需要传递不同的插槽名称,实现更灵活的插槽内容传递。

  1. 作用域插槽

作用域插槽(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>

在上面的例子中,子组件通过slotslotData对象传递给插槽内容,父组件通过作用域插槽接收这个数据并进行渲染。

注意:在 Vue 3 中,作用域插槽的语法有所简化,可以直接在template标签上使用#default来定义默认插槽(最好使用显示的默认插槽防止歧义,即使用<template #default>),并通过解构语法获取传递的数据。

结论

本文介绍了八种常用的Vue组件通信方式,根据具体的应用场景和需求,选择合适的通信方式可以提高代码的可维护性和可读性。Vue组件传参的方式多种多样,本文只介绍了几种常见的方法,实际可以有更多的变通和组合使用,灵活运用这些技术可以帮助开发者更好地管理组件之间的通信。

参考资料

Licensed under CC BY-NC-SA 4.0