第 117 章:Props 与 Emits 深入
学习目标
- 深入掌握 Props 的类型、校验、默认值
- 掌握 Emits 的各种用法与校验
- 学会复杂的 props 模式与最佳实践
- 区分 v-model 与 .sync 的替代方案
一、Props 完整指南
1.1 类型定义方式
写法 1:纯类型声明(推荐)
typescript
defineProps<{
msg: string;
count?: number;
items: string[];
user: { id: number; name: string };
onChange?: (val: string) => void;
}>();写法 2:运行时声明(支持默认值)
typescript
const props = defineProps({
msg: String,
count: {
type: Number,
default: 0
},
items: {
type: Array as PropType<string[]>,
default: () => []
}
});1.2 类型与默认值
typescript
// 用 withDefaults 给类型化的 props 添加默认值
interface Props {
msg?: string;
count?: number;
items?: string[];
visible?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
msg: 'Hello',
count: 0,
items: () => [],
visible: false
});1.3 PropType 复杂类型
typescript
import { PropType } from 'vue';
interface User {
id: number;
name: string;
}
const props = defineProps({
user: Object as PropType<User>,
users: Array as PropType<User[]>
});二、Props 校验规则
2.1 支持的类型
typescript
defineProps({
// 基本类型
str: String,
num: Number,
bool: Boolean,
arr: Array,
obj: Object,
date: Date,
fn: Function,
sym: Symbol,
// 多个类型
value: [String, Number],
// 必填
required: {
type: String,
required: true
},
// 默认值
default: {
type: Number,
default: 100
},
// 对象/数组默认值必须用工厂函数
list: {
type: Array,
default: () => []
},
obj: {
type: Object,
default: () => ({})
},
// 自定义校验
age: {
type: Number,
validator: (val: number) => val >= 0 && val <= 150
}
});2.2 布尔转换
vue
<script setup>
defineProps<{ disabled?: boolean }>();
</script>
<template>
<!-- 以下两种写法等价 -->
<MyButton disabled />
<MyButton :disabled="true" />
</template>三、Props 高级用法
3.1 单向数据流
vue
<script setup lang="ts">
const props = defineProps<{ count: number }>();
// ❌ 不要直接修改 props
props.count++; // 警告
// ✅ 拷贝到本地
const localCount = ref(props.count);
// ✅ 计算属性
const doubled = computed(() => props.count * 2);
</script>3.2 父组件监听子组件 props 变化
vue
<!-- Parent.vue -->
<script setup>
import { ref, watch } from 'vue';
import Child from './Child.vue';
const childData = ref('');
watch(childData, (newVal) => {
console.log('子组件数据变了', newVal);
});
</script>
<template>
<Child v-model="childData" />
</template>3.3 Props 解构
vue
<script setup lang="ts">
// ❌ 普通解构丢失响应式
const props = defineProps<{ count: number }>();
const { count } = props; // 普通 number
// ✅ 用 toRefs
import { toRefs } from 'vue';
const props = defineProps<{ count: number; name: string }>();
const { count, name } = toRefs(props);
// count.value, name.value
</script>
<template>
<!-- 模板中直接用 -->
<p>{{ count }} - {{ name }}</p>
</template>四、Emits 完整指南
4.1 基础语法
typescript
// 写法 1:类型签名(推荐)
const emit = defineEmits<{
increment: [count: number];
change: [value: string];
submit: [];
}>();
// 写法 2:对象形式
const emit = defineEmits({
increment: (count: number) => true,
change: null
});4.2 触发事件
typescript
emit('increment', 1);
emit('change', 'new value');
emit('submit');4.3 事件校验
typescript
const emit = defineEmits({
// 返回 false 时报警告
submit: ({ id, name }: { id: number; name: string }) => {
if (!id || !name) {
console.warn('id 和 name 必填');
return false;
}
return true;
}
});4.4 组件实例事件
vue
<!-- 父组件用 ref 访问子组件 -->
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const childRef = ref<InstanceType<typeof Child> | null>(null);
const callChild = () => {
childRef.value?.focus();
};
</script>
<template>
<Child ref="childRef" />
</template>五、组件 v-model 深入
5.1 v-model 原理
v-model="x" 实际上是 :modelValue="x" + @update:modelValue="x = $event" 的语法糖。
vue
<!-- 等价关系 -->
<Child v-model="x" />
<Child :modelValue="x" @update:modelValue="x = $event" />5.2 自定义 v-model 名称
vue
<!-- Child.vue -->
<script setup lang="ts">
defineProps<{ search: string }>();
defineEmits<{ 'update:search': [value: string] }>();
</script>
<template>
<input :value="search" @input="$emit('update:search', $event.target.value)" />
</template>vue
<!-- Parent.vue -->
<Child v-model:search="searchText" />5.3 多个 v-model
vue
<!-- Child.vue -->
<script setup lang="ts">
defineProps<{ name: string; age: number }>();
defineEmits<{
'update:name': [value: string];
'update:age': [value: number];
}>();
</script>vue
<Child v-model:name="userName" v-model:age="userAge" />5.4 v-model + 自定义修饰符
vue
<!-- Child.vue -->
<script setup lang="ts">
const props = defineProps<{
modelValue: string;
modelModifiers?: { capitalize?: boolean; upper?: boolean };
}>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
const update = (e: Event) => {
let value = (e.target as HTMLInputElement).value;
if (props.modelModifiers?.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1);
}
if (props.modelModifiers?.upper) {
value = value.toUpperCase();
}
emit('update:modelValue', value);
};
</script>
<template>
<input :value="modelValue" @input="update" />
</template>vue
<MyInput v-model.capitalize="text" v-model.upper="otherText" />六、实战案例:通用表单组件
6.1 FormField.vue
vue
<script setup lang="ts">
interface Props {
modelValue: string;
label: string;
type?: 'text' | 'password' | 'email';
placeholder?: string;
error?: string;
required?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
type: 'text',
required: false
});
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
const onInput = (e: Event) => {
emit('update:modelValue', (e.target as HTMLInputElement).value);
};
</script>
<template>
<div class="form-field">
<label>
{{ label }}
<span v-if="required" class="required">*</span>
</label>
<input
:type="type"
:value="modelValue"
:placeholder="placeholder"
@input="onInput"
:class="{ 'has-error': error }"
/>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>6.2 使用
vue
<script setup lang="ts">
import { ref } from 'vue';
import FormField from './FormField.vue';
const username = ref('');
const password = ref('');
const errors = ref<Record<string, string>>({});
const submit = () => {
errors.value = {};
if (!username.value) errors.value.username = '用户名必填';
if (password.value.length < 6) errors.value.password = '密码至少 6 位';
};
</script>
<template>
<form @submit.prevent="submit">
<FormField
v-model="username"
label="用户名"
required
:error="errors.username"
/>
<FormField
v-model="password"
label="密码"
type="password"
required
:error="errors.password"
/>
<button type="submit">提交</button>
</form>
</template>七、组件通信模式
7.1 通信方式对比
7.2 选择建议
| 场景 | 推荐方式 |
|---|---|
| 父传子(简单) | Props |
| 子传父(简单) | Emits |
| 双向绑定 | v-model |
| 多层传递 | provide/inject |
| 全局共享 | Pinia |
| 临时事件 | mitt 等事件总线 |
八、常见错误
8.1 props 类型错误
vue
<!-- ❌ 父组件传错类型 -->
<Child :count="'10'" /> <!-- Child 期望 number -->
<!-- ✅ 修正 -->
<Child :count="10" />8.2 解构丢失响应式
typescript
const props = defineProps<{ count: number }>();
const { count } = props; // 丢失响应式8.3 触发未声明的事件
typescript
// ❌ 没有声明就 emit
const emit = defineEmits<{ change: [v: string] }>();
emit('click'); // 警告:未声明的事件九、本章小结
| 要点 | 关键 |
|---|---|
| Props 类型 | defineProps<{}>() 或 defineProps({}) |
| 默认值 | withDefaults() |
| 类型转换 | PropType<T> |
| Emits | defineEmits<{}>() + emit() |
| v-model | :modelValue + @update:modelValue |
| 多 v-model | v-model:xxx |
| 单向数据流 | props 只读,不能直接改 |
动手练习
- 复杂表单:实现一个用户注册表单,包含验证和错误提示
- 自定义 v-model:实现一个数字输入框,支持
.min.max修饰符 - 事件校验:写一个 emits,带参数类型校验
- 父监听子变化:父组件监听子组件 props 变化并响应
推荐阅读
- 📖 Vue 3 Props — 官方文档
- 📖 Vue 3 Events — 事件文档
- 🌐 Vue 3 Custom v-model — 自定义 v-model
下一章:第 118 章:插槽与作用域 →