第106课:组件通信(上)——Props、defineProps、defineEmits、v-model 实现
Vue 组件树中的通信遵循单向数据流原则:数据从父组件通过 Props 向下流动到子组件,子组件通过事件(Emits) 向上通知父组件发生了某些事情。这一“Props down, Events up”的模式是 Vue 组件化架构的基石,它使得数据流可预测、易于调试。在此基础上,Vue 3 提供了 v-model 的增强语法,允许父子组件之间实现便捷的双向绑定——这本质上仍是 Props 和 Emits 的组合,但语法上更为简洁。理解 Props 的类型校验、单向数据流约束、Emits 的事件声明和验证、以及 v-model 的底层实现,是构建可复用、类型安全的 Vue 组件的基础。本节课将逐一拆解这四个核心概念,并通过实战构建一个完整的自定义表单控件来串联它们。
1. Props:父组件向子组件传递数据
Props 是父组件传递给子组件的只读数据。在组合式 API 中,使用 defineProps 宏来声明组件接收的 Props。Vue 会根据声明的类型在开发模式下进行类型校验,并给出控制台警告。
1.1 defineProps 的基本语法
1 | <!-- ChildComponent.vue --> |
关键点:
defineProps是一个编译宏——它不需要从 Vue 中导入,Vue 的 SFC 编译器会自动处理它。它仅在<script setup>中可用。defineProps返回一个只读的响应式对象。在模板中可以直接访问属性(自动解包),在<script>中也可以通过props.xxx访问。- TypeScript 纯类型声明(
defineProps<Props>())是最简洁的方式,但这种方式不支持运行时默认值。如需默认值,使用withDefaults(defineProps<Props>(), defaults)。 - 运行时声明(传入对象字面量)也支持,使用
type、default、required、validator等选项,类似于选项式 API 的props定义。适用于非 TypeScript 项目或需要自定义验证器的场景。
1 | <script setup> |
1.2 Props 的单向数据流原则
绝对不要在子组件内部直接修改 Props 的值。Props 是父组件传递下来的只读数据,修改它会破坏单向数据流,导致数据不一致,并且 Vue 会在开发模式下发出警告。
1 | <script setup> |
如果确实需要基于 Props 的初始值创建一个本地状态(例如,弹窗默认打开状态由父组件控制,但子组件内部也可以关闭),应当将 Props 的初始值赋给一个本地的 ref 或 reactive,然后操作该本地状态。
1 | <script setup> |
1.3 Props 的校验:validator 选项
运行时声明的 Props 支持自定义验证函数。验证失败时 Vue 会在控制台输出警告(仅在开发模式)。
1 | <script setup> |
注意:自定义验证器返回 false 时仅发出警告,不会阻止组件渲染或抛出异常。这是开发时的辅助工具,不应依赖它来保证数据正确性。
2. defineEmits:子组件向父组件通信
Emits 是子组件向父组件发送消息的机制。在组合式 API 中,使用 defineEmits 宏声明组件可以触发哪些事件及其参数类型。
2.1 基本语法
1 | <!-- ChildComponent.vue --> |
父组件中监听事件:
1 | <ChildComponent @submit="onSubmit" @cancel="onCancel" /> |
2.2 事件验证
defineEmits 也支持运行时验证——通过对象语法为每个事件提供验证函数。验证函数接收事件参数,返回 true(验证通过)或 false(验证失败,Vue 发出警告)。
1 | <script setup> |
与 Props 验证类似,Emit 验证失败仅发出警告,不阻止事件触发。
2.3 Emits 的最佳实践
- 始终声明组件触发的所有事件(通过
defineEmits),即使 TypeScript 类型推导无法覆盖,也要在运行时声明中列出。这能生成更好的组件文档,且 Vue 会自动将声明的事件从$attrs中排除,避免传递到根元素。 - 事件名使用 camelCase,父组件中使用 kebab-case 监听。这是 Vue 官方推荐的一致命名风格。
- 携带明确的事件参数:事件参数应包含足够信息,避免父组件需要通过
$event隐式获取数据。例如emit('update', id, newValue)或emit('update', { id, newValue })比仅触发事件让父组件猜测更清晰。
3. v-model:Props 与 Emits 的语法糖
v-model 是 Vue 中最常用的双向绑定指令。在 Vue 3 中,v-model 的底层机制被统一为:一个名为 modelValue 的 Prop + 一个名为 update:modelValue 的 Emit。同时 Vue 3 支持多个 v-model 绑定和自定义修饰符。
3.1 单个 v-model 的底层实现
1 | <!-- 父组件 --> |
子组件实现:
1 | <!-- CustomInput.vue --> |
3.2 多个 v-model 绑定
Vue 3 允许在一个组件上使用多个 v-model,每个绑定到一个特定名称的 Prop 和 Emit。
1 | <!-- 父组件 --> |
子组件实现:
1 | <!-- UserForm.vue --> |
命名规则:v-model:propName 对应 Prop propName 和 Emit update:propName。默认的 v-model(无参数)对应 modelValue 和 update:modelValue。
3.3 v-model 修饰符
Vue 3 允许为 v-model 定义自定义修饰符,例如 .trim、.capitalize。修饰符通过 modelModifiers Prop 传递(默认 v-model)或 {propName}Modifiers(具名 v-model)。
1 | <!-- 父组件使用修饰符 --> |
子组件处理修饰符:
1 | <script setup> |
修饰符的命名规则:
- 默认
v-model的修饰符通过modelModifiersprop 传递。 - 具名
v-model:name的修饰符通过nameModifiersprop 传递。 - 修饰符对象中包含对应修饰符名称为键,值为
true的属性(如果父组件使用了该修饰符)。
4. v-model 与计算属性的配合
当子组件的 v-model 逻辑较复杂时(如在 emit 之前需要转换数据),可以结合可写计算属性简化代码。可写计算属性的 get 返回 Prop 值,set 中调用 emit。
1 | <script setup> |
这种模式的好处是**子组件内部可以像操作本地状态一样使用 value**(包括使用 v-model 绑定到原生元素),而无需手动编写事件处理函数。适用于 Props 到 Emit 的 1:1 映射场景。
5. 综合实战:自定义星级评分组件
以下示例将 Props、defineEmits 和 v-model 整合为一个可复用的星级评分组件。
1 | <!-- StarRating.vue --> |
父组件使用:
1 | <script setup> |
设计要点:
- 通过
v-model绑定评分值,父组件无需关心内部实现。 - 支持
max、readonlyProps 配置组件行为。 - 通过
modelModifiers.half实现半星修饰符,允许.5评分。 - 子组件完全通过
emit('update:modelValue')通知父组件更新,遵循单向数据流原则。 - 只读模式下禁用点击,CSS 隐藏光标指示。
课后练习
一、概念自测(选择题 / 填空题)
(单选) 在 Vue 3
<script setup>中,声明组件接收的 Props 应使用哪个编译宏?
A.defineProps
B.withDefaults
C.defineEmits
D.defineComponent(单选) Vue 3 中,
v-model="value"的底层等价写法是什么?
A.:value="value" @change="value = $event"
B.:modelValue="value" @update:modelValue="value = $event"
C.:prop="value" @input="value = $event.target.value"
D.:value="value" @update="value = $event"(填空) Vue 3 中,要在一个组件上绑定多个
v-model,需要使用______语法指定不同的绑定名称。(多选) 关于 Vue 3 Props 的说法,哪些是正确的?
A. 子组件可以直接修改 Props 的值以更新父组件状态。
B.defineProps返回的对象在<script setup>中是响应式的。
C. Props 的默认值如果是对象或数组,必须使用工厂函数返回。
D. 使用 TypeScript 泛型声明 Props 时,可以通过withDefaults设置默认值。
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要实现一个颜色选择器组件(ColorPicker)。要求如下:
- 使用
v-model绑定当前颜色值(十六进制字符串,如#ff0000)。 - 提供 12 个预设颜色块,点击预设颜色更新
v-model。 - 提供一个文本输入框,允许用户手动输入十六进制颜色值,实时更新
v-model。 - 使用
v-model:opacity绑定第二个值:不透明度(0~1 的数字),通过滑块调节。 - 使用
defineProps和withDefaults定义 Props(如presetColors数组)。 - 使用
defineEmits声明所有事件。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 Vue 3 单文件组件。提示词中需明确指定 v-model 的默认和具名绑定、Props 和 Emits 的类型定义、以及颜色输入校验逻辑。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请创建一个 Vue 3 颜色选择器组件。需要创建文件
src/components/ColorPicker.vue:
- 使用
<script setup lang="ts">。defineProps声明:modelValue: string(颜色值,默认'#000000')、opacity: number(不透明度,默认1)、presetColors: string[](默认['#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff', '#ff6600', '#6600ff', '#00ff66', '#666666'])。使用withDefaults设置默认值。defineEmits声明:'update:modelValue': [value: string]和'update:opacity': [value: number]。- 模板布局:左侧预设颜色网格(
display: grid),每个色块点击时emit('update:modelValue', color)。右侧上方显示当前颜色的预览方块(背景色为modelValue,叠加不透明度)。下方文本输入框绑定一个本地ref(初始值为modelValue),监听@input时校验输入是否为有效十六进制颜色(正则/^#[0-9a-fA-F]{6}$/),有效则emit('update:modelValue', value)。下方不透明度滑块(type="range" min="0" max="1" step="0.01"),:value="opacity",@input时emit('update:opacity', parseFloat(e.target.value))。- 所有颜色色块和预览块添加边框和圆角样式。添加 JSDoc 注释。完成后输出完整文件内容。
四、面试真题与参考答案
题目(阿里前端面试题):
请详细解释 Vue 3 中 Props 和 Emits 的设计原则(单向数据流),以及
v-model是如何基于这两个概念实现的。如果父组件通过 Props 传递了一个对象给子组件,子组件直接修改了该对象的内部属性,这会破坏单向数据流吗?Vue 会如何反应?请说明原因。
参考答案:
Vue 的 Props 和 Emits 遵循“Props down, Events up”的单向数据流原则:数据从父组件通过 Props 向下流动到子组件,子组件不能直接修改 Props,而是通过触发事件(Emits)将修改意图告知父组件,由父组件更新自己的数据源并重新通过 Props 传递新值。这种模式确保了数据来源的可追溯性,任何状态变更都有明确的“所有者”,避免了组件间隐式的相互影响。
v-model 是 Props 和 Emits 的语法糖。默认的 v-model 展开为 :modelValue="value" @update:modelValue="value = $event"。具名 v-model:name 展开为 :name="value" @update:name="value = $event"。子组件通过 defineProps 接收对应的 Prop,通过 defineEmits 触发对应的更新事件。
如果父组件传递给子组件一个对象,子组件直接修改该对象内部的属性(如 props.user.name = 'Bob'),这在技术上不会报错,因为 Vue 的 Props 只读保护是浅层的——它仅阻止替换整个 Prop 的引用(props.user = newObject),但不阻止修改嵌套对象内部的属性。然而,这破坏了单向数据流的可预测性——父组件不会收到任何通知,它持有的数据源被悄无声息地修改了,导致状态不一致。Vue 在开发模式下不会对此发出警告(因为深层监听开销巨大),但这是公认的反模式。正确的做法仍然是通过 Emit 通知父组件修改,或使用 Pinia 等状态管理方案管理共享状态。如果确实需要就地编辑对象,应当将对象深拷贝到本地 ref,编辑完成后再通过 Emit 统一提交。
课后练习答案
一、概念自测答案
A
- 解析:
defineProps是用于声明组件 Props 的编译宏。withDefaults用于设置默认值,defineEmits用于声明事件,defineComponent是选项式 API 的辅助函数。
- 解析:
B
- 解析:Vue 3 中
v-model的底层是:modelValueProp 和@update:modelValueEmit 的组合。A 是 Vue 2 的行为;C、D 语法不准确。
- 解析:Vue 3 中
v-model:name- 解析:
v-model:propName用于创建具名v-model,对应 ProppropName和 Emitupdate:propName。
- 解析:
B、C、D
- 解析:A 错误,子组件不能直接修改 Props,这违反了单向数据流原则。B 正确,
defineProps返回一个只读的响应式对象。C 正确,对象/数组默认值必须使用工厂函数,以避免多个实例共享同一引用。D 正确,withDefaults(defineProps<Props>(), defaults)是 TypeScript 泛型声明 + 默认值的标准写法。
- 解析:A 错误,子组件不能直接修改 Props,这违反了单向数据流原则。B 正确,
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请用 Vue 3<script setup lang="ts">实现一个颜色选择器组件ColorPicker.vue。要求:
- 使用
v-model绑定颜色值(十六进制),v-model:opacity绑定不透明度。defineProps+withDefaults定义 Props(modelValue, opacity, presetColors)。defineEmits声明update:modelValue和update:opacity。- 预设颜色块点击更新值,文本输入框校验后更新值,滑块更新不透明度。
- 输出完整单文件组件。”