在 Vue3 中,父子组件通信是组件化开发的核心机制,以下是详细解析和最佳实践方案:
一、核心通信方式
1. 父传子:Props
<!-- 父组件 -->
<template>
<Child :title="parentTitle" :config="{ size: 'large' }" />
</template>
<script setup>
import Child from './Child.vue'
const parentTitle = ref('Hello Vue3')
</script>
<!-- 子组件 -->
<script setup>
const props = defineProps({
title: {
type: String,
required: true
},
config: {
type: Object as PropType<{ size: string }>,
default: () => ({ size: 'medium' })
}
})
</script>
最佳实践:
- 使用 TypeScript 类型声明 (
PropType
) - 复杂对象提供合理的默认值
- 避免直接修改 props
2. 子传父:自定义事件
<!-- 子组件 -->
<script setup>
const emit = defineEmits<{
(e: 'update:title', value: string): void
(e: 'submit', payload: FormData): void
}>()
const handleClick = () => {
emit('update:title', 'New Title')
}
</script>
<!-- 父组件 -->
<template>
<Child @update:title="handleUpdate" @submit="handleSubmit" />
</template>
最佳实践:
- 使用 TypeScript 定义严格的事件签名
- 自定义事件名使用 kebab-case (如
update-title
) - 复杂数据通过对象包装传递
二、高级场景解决方案
1. 双向绑定(v-model 增强)
<!-- 父组件 -->
<template>
<Child v-model:username="user.name" v-model:age="user.age" />
</template>
<!-- 子组件 -->
<script setup>
const props = defineProps(['username', 'age'])
const emit = defineEmits(['update:username', 'update:age'])
const updateName = (val) => {
emit('update:username', val.trim())
}
</script>
特性:
- 支持多个 v-model 绑定
- 可添加数据转换逻辑
2. 复杂对象传递
// 父组件
interface UserData {
id: number
profile: {
name: string
avatar?: string
}
}
const user = reactive<UserData>({
id: 1,
profile: { name: 'Alice' }
})
<!-- 子组件 -->
<script setup lang="ts">
interface Props {
user: UserData
}
const props = defineProps<Props>()
</script>
注意:直接修改 reactive 对象属性会触发响应,但应通过事件通知父组件
三、常见问题与解决方案
1. Props 异步数据问题
<!-- 父组件 -->
<Child v-if="loadedData" :data="asyncData" />
<!-- 子组件 -->
<script setup>
watch(() => props.data, (newVal) => {
// 处理数据变化
}, { immediate: true })
</script>
2. 事件验证
defineEmits({
'update-data': (payload: { id: number; value: string }) => {
if (!payload.id || !payload.value) {
console.warn('Invalid payload format')
return false
}
return true
}
})
3. 大型数据结构优化
// 使用 shallowRef 避免深层响应式开销
const largeData = shallowRef(bigJsonData)
// 子组件需要响应式访问时
const partialData = computed(() => props.largeData.value.section)
四、最佳实践总结
-
数据流原则:
- 单向数据流为主,复杂场景使用 Pinia
- 避免直接修改 props (除 v-model 约定情况)
-
类型安全:
// Props 类型声明 interface Props { id: number items: Array<{ id: number; text: string }> } const props = defineProps<Props>()
-
性能优化:
- 大对象使用
shallowRef/shallowReactive
- 必要时使用
toRefs
解构 props
- 大对象使用
-
调试友好:
// 开发环境添加验证 defineProps({ items: { type: Array, validator: (val) => val.every(item => 'id' in item)) } })
-
组合式 API 模式:
// 使用 composable 封装逻辑 export function useFormValidation(props) { const isValid = computed(() => { return props.value.length > 0 }) return { isValid } }
五、典型场景示例
表单验证组件
<!-- 父组件 -->
<FormInput
v-model="email"
:rules="[required, emailFormat]"
@validation="handleValidation"
/>
<!-- 子组件 -->
<script setup lang="ts">
const props = defineProps({
modelValue: String,
rules: Array as PropType<Array<(val: string) => boolean>>
})
const emit = defineEmits(['update:modelValue', 'validation'])
const validate = () => {
const isValid = props.rules.every(rule => rule(props.modelValue))
emit('validation', isValid)
}
</script>
通过合理运用这些模式,可以构建出既符合 Vue3 响应式特性,又具备良好可维护性的组件体系。