组件通信没有唯一标准答案,关键是看数据属于谁、要经过几层、是否要跨页面共享。日常 Vue3 项目里,父子组件优先用 props 和 emit;层级较深但作用范围有限时用 provide/inject;真正跨页面的公共状态再交给 Pinia。
父组件传给子组件:props
用户信息由父组件管理,子组件只负责展示时,直接传 props 最合适。数据流是从上到下的,阅读起来也最直接。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| <!-- UserCard.vue -->
<script setup lang="ts">
interface User {
id: number
name: string
role: string
}
defineProps<{ user: User }>()
</script>
<template>
<article>
<strong>{{ user.name }}</strong>
<span>{{ user.role }}</span>
</article>
</template>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| <!-- UserList.vue -->
<script setup lang="ts">
import UserCard from './UserCard.vue'
const currentUser = {
id: 1,
name: '林达',
role: '管理员',
}
</script>
<template>
<UserCard :user="currentUser" />
</template>
|
子组件不要直接修改 props。这会破坏单向数据流,也容易在控制台出现警告。
子组件通知父组件:emit
删除、保存、切换状态这类动作,由子组件触发、父组件决定怎么处理,适合用事件回传。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| <!-- UserCard.vue -->
<script setup lang="ts">
const emit = defineEmits<{
remove: [id: number]
}>()
const props = defineProps<{ id: number; name: string }>()
</script>
<template>
<button type="button" @click="emit('remove', props.id)">
删除 {{ props.name }}
</button>
</template>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| <!-- UserList.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const users = ref([
{ id: 1, name: '林达' },
{ id: 2, name: '小王' },
])
const removeUser = (id: number) => {
users.value = users.value.filter((user) => user.id !== id)
}
</script>
<template>
<UserCard
v-for="user in users"
:key="user.id"
v-bind="user"
@remove="removeUser"
/>
</template>
|
事件名最好描述行为,例如 save、remove、change-status。不要用 callback 这种看不出意图的名字。
需要双向绑定时:v-model
表单组件最常见。Vue3 的 v-model 本质上是一个 modelValue 属性和一个 update:modelValue 事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <!-- SearchInput.vue -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
</script>
<template>
<input
:value="props.modelValue"
placeholder="搜索用户"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</template>
|
父组件就可以像使用原生输入框一样使用它:
1
| <SearchInput v-model="keyword" />
|
跨多层但只在局部使用:provide / inject
主题配置、表单上下文、页面级权限等数据,经过多层组件逐级传递会很麻烦。这时可以由页面根组件提供,再由后代组件取用。
1
2
3
4
5
| // 页面根组件
import { provide, ref } from 'vue'
const readonly = ref(false)
provide('formReadonly', readonly)
|
1
2
3
4
| // 深层子组件
import { inject, type Ref } from 'vue'
const readonly = inject<Ref<boolean>>('formReadonly')
|
这适合“局部共享”,不建议把所有数据都塞进去。过多的注入会让数据来源变得不清楚。
跨页面共享:Pinia
登录用户、权限、全局主题、购物车这类状态需要多个页面共用,才值得放进 Pinia。页面临时的弹窗开关、单个表单字段仍然留在组件内更轻。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: '',
token: '',
}),
actions: {
setUser(name: string, token: string) {
this.name = name
this.token = token
},
},
})
|
选择建议
- 一层父子:
props + emit。 - 表单值双向同步:
v-model。 - 多层组件的局部上下文:
provide/inject。 - 跨路由、跨模块的共享状态:Pinia。
先从最简单的方案开始。通信方式越重,维护成本越高;数据范围越小,代码通常越容易整理。