Vue3父子组件通信指南:Props、Emits、Expose 与 v-model 实践
Vue 3 的核心原则是单向数据流:数据从父组件流向子组件,子组件通过事件通知父组件进行修改。基于这一原则,Vue 提供了多种通信方式,各自适用于不同场景。本文将以 <script setup> 语法为基础,系统梳理这些方式,并附上实用的避坑指南。
一、父传子:Props 的正确用法
1.1 基本传递
父组件通过在子组件标签上添加属性来传递数据。使用 : 前缀表示动态绑定,传递的是 JavaScript 表达式;不加前缀则传递静态字符串。
<!-- 父组件 Father.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const message = ref('来自父亲的问候')
const userInfo = { name: '小明', age: 18 }
</script>
<template>
<Child
:msg="message" <!-- 动态绑定变量 -->
:user="userInfo" <!-- 动态绑定对象 -->
count="123" <!-- 静态字符串,等价于 :count="'123'" -->
/>
</template>
1.2 子组件接收
子组件使用 defineProps 宏来声明接收的属性:
<!-- 子组件 Child.vue -->
<script setup>
// 方式一:简单数组声明(仅适用于快速原型)
const props = defineProps(['msg', 'user', 'count'])
// 方式二:对象声明,支持类型校验和默认值(推荐)
const props = defineProps({
msg: {
type: String,
required: true
},
user: {
type: Object,
default: () => ({}) // 对象/数组的默认值必须用工厂函数
},
count: {
type: String,
default: '0'
}
})
// 使用:通过 props.xxx 访问
console.log(props.msg)
</script>
<template>
<p>{{ msg }}</p>
<p>{{ user.name }}</p>
</template>
TypeScript 用户:可以使用基于类型的声明,更加简洁且类型安全。
const props = defineProps<{ msg: string user?: { name: string; age: number } count?: string }>()
重要原则:Props 是只读的
子组件绝对不能直接修改 props 的值:
// ❌ 错误!这会触发 Vue 的警告
props.msg = '新值'
// ✅ 正确做法:通过事件通知父组件修改
const emit = defineEmits(['updateMsg'])
emit('updateMsg', '新值')
这个限制确保了数据流的可预测性,是 Vue 响应式系统的核心设计之一。
二、子传父:Emits 事件机制
当子组件需要向父组件传递数据或通知状态变化时,使用 defineEmits 触发自定义事件。
2.1 子组件触发事件
<!-- 子组件 Child.vue -->
<script setup>
// 声明事件(推荐使用对象语法进行校验)
const emit = defineEmits({
// 简单声明
updateMsg: null,
// 带校验:可以验证传递的参数是否符合预期
sendData: (payload) => {
if (payload && typeof payload === 'object' && 'id' in payload) {
return true
}
console.warn('sendData 事件参数格式不正确')
return false
}
})
function handleClick() {
emit('updateMsg', '子组件发来的新消息')
}
function sendComplex() {
emit('sendData', { id: 1, content: '复杂数据' })
}
</script>
<template>
<button @click="handleClick">发送消息</button>
<button @click="sendComplex">发送对象</button>
</template>
2.2 父组件监听事件
父组件使用 @事件名 语法监听,事件名在模板中统一使用 kebab-case(短横线命名)格式:
<!-- 父组件 Father.vue -->
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const parentMsg = ref('初始内容')
function onUpdateMsg(newValue) {
parentMsg.value = newValue // 在这里真正更新数据
}
function onSendData(data) {
console.log(data) // { id: 1, content: '复杂数据' }
}
</script>
<template>
<Child
@update-msg="onUpdateMsg"
@send-data="onSendData"
/>
<p>父组件收到:{{ parentMsg }}</p>
</template>
三、父调子:Expose + ref
某些场景下,父组件需要直接访问子组件的实例,调用其方法或读取其数据。例如:重置表单、聚焦输入框、触发子组件的刷新逻辑等。
3.1 子组件暴露内容
子组件使用 defineExpose 宏显式暴露属性或方法:
<!-- 子组件 Child.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const internalData = ref('内部私有数据') // 未暴露,父组件无法访问
function increment() {
count.value++
}
function reset() {
count.value = 0
}
// 只暴露 increment 方法和 count 属性
defineExpose({
count,
increment
})
</script>
<template>
<p>子组件 count: {{ count }}</p>
</template>
3.2 父组件通过 ref 访问
父组件使用 ref 获取子组件实例,并通过 .value 访问暴露的内容:
<!-- 父组件 Father.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
// ref 变量名必须与模板中的 ref 属性值一致
const childRef = ref(null)
function callChildMethod() {
// 使用可选链确保安全访问
childRef.value?.increment()
console.log('当前子组件 count:', childRef.value?.count)
}
</script>
<template>
<Child ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</template>
使用建议
expose 应作为最后的选择,而非首选方案。过度使用会破坏组件的封装性,使组件间的耦合度升高。在大部分场景中,优先考虑使用 props + emits 完成通信。
四、v-model:双向绑定的语法糖
v-model 本质上是 :modelValue 与 @update:modelValue 的组合语法糖,特别适合表单控件或需要双向绑定的自定义组件。
4.1 基础用法
<!-- 父组件 -->
<Child v-model="searchText" />
<!-- 完全等价于 -->
<Child :modelValue="searchText" @update:modelValue="searchText = $event" />
子组件实现:
<!-- 子组件 -->
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const updateValue = (event) => {
emit('update:modelValue', event.target.value)
}
</script>
<template>
<input :value="modelValue" @input="updateValue" />
</template>
4.2 多个 v-model 绑定
Vue 3 支持为 v-model 指定参数名,实现多个双向绑定:
<!-- 父组件 -->
<Child
v-model:title="bookTitle"
v-model:author="bookAuthor"
/>
子组件对应接收:
<script setup>
defineProps(['title', 'author'])
const emit = defineEmits(['update:title', 'update:author'])
</script>
4.3 自定义修饰符
<!-- 父组件:使用 capitalize 修饰符 -->
<Child v-model.capitalize="text" />
子组件可以通过 modelModifiers prop 获取修饰符状态:
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])
const updateValue = (e) => {
let value = e.target.value
// 根据修饰符处理值
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
五、方式对比与选型建议
| 通信方式 | 方向 | 核心 API | 推荐使用场景 |
|---|---|---|---|
| Props | 父 → 子 |
defineProps
|
父组件向子组件传递数据,用于展示或配置。 |
| Emits | 子 → 父 |
defineEmits
|
子组件向父组件发送事件通知或回传数据。 |
| Expose + ref | 父 → 子(调用) |
defineExpose, ref
|
父组件需要主动触发子组件的特定方法(命令式调用)。 |
| v-model | 双向绑定 |
:modelValue + @update:modelValue
|
表单控件或自定义输入组件,需要同步更新状态。 |
选型决策树
需要父→子传递数据?
├─ 是 → 使用 Props
└─ 否 → 需要子→父通知?
├─ 是 → 使用 Emits
└─ 否 → 需要父主动调用子方法?
├─ 是 → 使用 Expose + ref
└─ 否 → 考虑是否真的需要通信
核心原则
尽可能保持数据流的单向性。Props 负责传入,Emits 负责传出,这是最清晰、最可维护的模式。
v-model是这一模式在表单场景下的优雅封装。而expose是"逃生舱",仅在必须进行命令式操作时使用。
- 上一篇:vue3开发常用的核心函数有哪些
- 下一篇:无