在 Vue 3 的组合式 API(Composition API)中,组件间的通信变得更加清晰、类型安全且易于维护。本文将系统梳理 defineProps
、defineEmits
和 defineExpose
三大核心 API 的使用方式,涵盖 JavaScript 与 TypeScript 环境下的差异,并结合实际场景说明。
一、defineProps
:接收父组件传值
1. 非 TypeScript 环境
- 基本用法:
- 通过对象形式定义属性,支持类型声明、默认值和验证规则。
const props = defineProps({ title: { type: String, // 类型检查 default: '默认标题', // 默认值 required: true // 必填校验 }, count: { type: Number, validator: (value) => value >= 0 // 自定义验证 } });
- 模板使用:直接通过属性名访问(
{{ title }}
)。 - JS 访问:通过
props.title
引用 。 type
:指定属性类型(String
,Number
,Boolean
,Array
,Object
,Function
,null
或自定义构造函数)default
:设置默认值required
:是否为必传项validator
:自定义验证函数(高级用法)
⚠️ 注意:简单类型(如字符串、数字)的
default
可直接赋值;复杂类型(对象、数组)必须使用函数返回,避免引用共享。
2. TypeScript 环境
(1) 基础类型定义(使用泛型)
<script setup lang="ts">
interface Props {
title: string
list?: number[] // 可选属性
userInfo: {
name: string
age: number
}
}
const props = defineProps<Props>()
</script>
(2) 设置默认值:withDefaults
由于 defineProps
在 TS 中是类型推导,不能直接在类型中写默认值,因此需要使用 withDefaults
辅助函数。
const props = withDefaults(
defineProps<{
title: string
list?: number[]
userInfo: {
name: string
age: number
}
}>(),
{
title: '默认标题',
list: () => [1, 2, 3],
userInfo: () => ({
name: '小明',
age: 18
})
}
)
✅ 关键点:
withDefaults
第一个参数必须是defineProps()
的调用结果。- 所有默认值中,对象和数组必须用函数返回,防止多个组件实例共享同一引用。
- 可选属性(
?
)也可以设置默认值。
二、defineEmits
:子组件向父组件传值
1. 定义事件
- 简单定义(无类型):
<script setup>
const emit = defineEmits(['onClick', 'onChange'])
// 触发事件
const handleClick = () => {
emit('onClick', '来自子组件的数据')
}
</script>
- TS 类型形式(推荐):
const emit = defineEmits<{ (e: 'onClick', name: string): void; // 带参数的事件 (e: 'update:count', value: number): void; // 可定义多个事件,格式:(事件名, 参数1, 参数2) => void }>();
2. 触发事件与父组件监听
- 子组件触发:通过
emit()
传递数据。<button @click="emit('onClick', '你好啊')">传递值</button>
- 父组件监听:使用
@事件名
接收数据。<ChildComponent @onClick="handleClick" />
const handleClick = (name: string) => { console.log(`收到子组件值:${name}`); // 输出:你好啊 };
✅ 优势:
- 编辑器自动提示事件名和参数
- 调用
emit
时参数类型错误会立即报错- 支持多个事件定义
三、defineExpose
:子组件暴露属性/方法
1. 子组件暴露内容
- 通过
defineExpose
显式暴露属性或方法:// 子组件 const name = 'neon'; const open = () => console.log('执行 open 方法'); defineExpose({ name, open });
2. 父组件调用暴露内容
- 定义 Ref 引用:为子组件实例声明类型。
// 父组件 import ChildComponent from './ChildComponent.vue'; const childRef = ref<InstanceType<typeof ChildComponent>>();
- 通过 Ref 调用:
<ChildComponent ref="childRef" /> <button @click="handleCallChild">调用子组件</button>
const handleCallChild = () => { console.log(childRef.value?.name); // 输出:neon childRef.value?.open(); // 输出:执行 open 方法 };
✅
InstanceType<typeof Component>
:获取组件的实例类型,确保调用安全。
3. 典型应用场景
常见于 UI 组件库(如 Element Plus、Vuetify)中的表单、弹窗等组件:例如表单组件通过 defineExpose 暴露 validate()(验证)、reset()(重置)等方法,父组件通过 ref 调用这些方法实现表单操作。
表单验证(Form 组件)
// 子组件:Form.vue
defineExpose({
validate: (callback: (valid: boolean) => void) => { /* 验证逻辑 */ },
reset: () => { /* 重置表单 */ }
})
// 父组件
const formRef = ref<InstanceType<typeof Form>>()
const submit = () => {
formRef.value?.validate((valid) => {
if (valid) {
console.log('表单验证通过,提交数据')
}
})
}
弹窗控制(Modal / Dialog)
// 子组件
defineExpose({
open,
close,
toggle
})
父组件可统一控制多个弹窗的显示隐藏,无需通过 v-model
或 props
反复传递状态。
四、对比 Vue 2 与 Vue 3
特性 | Vue 2 | Vue 3 |
---|---|---|
Props 定义 | props 选项 | defineProps 函数 |
类型支持 | 无 TS 推断 | 原生 TS 支持 |
默认值设置 | default 属性 | withDefaults (TS 专属) |
事件定义 | emits 选项 | defineEmits + 类型声明 |
暴露方法 | this.$refs 隐式访问 | defineExpose 显式暴露 |
优势:Vue 3 的 Composition API 提供更清晰的类型安全性和代码组织能力 。
五、注意事项
- 优先使用 TypeScript:
- 获得完整的类型推导与错误提示。
- 复杂类型默认值用函数返回:
- 避免引用共享问题 。
- 事件命名规范:
- 事件命名使用小驼峰或 kebab-case(如
updateCount
),父组件监听时可写为@update-count
。
- 事件命名使用小驼峰或 kebab-case(如
- Ref 安全访问:
- 调用
childRef.value
前需用可选链(?.
)避免空值错误 。
- 调用
- 结合
v-model
实现双向绑定:- 基于
props + emit('update:xxx')
实现。
- 基于