在 Vue 3 的组合式 API(Composition API)中,组件间的通信变得更加清晰、类型安全且易于维护。本文将系统梳理 defineProps、defineEmits 和 defineExpose 三大核心 API 的使用方式,涵盖 JavaScript 与 TypeScript 环境下的差异,并结合实际场景说明。
一、defineProps:接收父组件传值
1. 非 TypeScript 环境
-
基本用法 :
- 通过对象形式定义属性,支持类型声明、默认值和验证规则。
javascriptconst 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) 基础类型定义(使用泛型)
typescript
<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 辅助函数。
typescript
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. 定义事件
- 简单定义(无类型):
javascript
<script setup>
const emit = defineEmits(['onClick', 'onChange'])
// 触发事件
const handleClick = () => {
emit('onClick', '来自子组件的数据')
}
</script>
-
TS 类型形式 (推荐):
typescriptconst emit = defineEmits<{ (e: 'onClick', name: string): void; // 带参数的事件 (e: 'update:count', value: number): void; // 可定义多个事件,格式:(事件名, 参数1, 参数2) => void }>();
2. 触发事件与父组件监听
-
子组件触发 :通过
emit()传递数据。html<button @click="emit('onClick', '你好啊')">传递值</button> -
父组件监听 :使用
@事件名接收数据。html<ChildComponent @onClick="handleClick" />typescriptconst handleClick = (name: string) => { console.log(`收到子组件值:${name}`); // 输出:你好啊 };
✅ 优势:
- 编辑器自动提示事件名和参数
- 调用
emit时参数类型错误会立即报错- 支持多个事件定义
三、defineExpose:子组件暴露属性/方法
1. 子组件暴露内容
-
通过
defineExpose显式暴露属性或方法:typescript// 子组件 const name = 'neon'; const open = () => console.log('执行 open 方法'); defineExpose({ name, open });
2. 父组件调用暴露内容
-
定义 Ref 引用 :为子组件实例声明类型。
typescript// 父组件 import ChildComponent from './ChildComponent.vue'; const childRef = ref<InstanceType<typeof ChildComponent>>(); -
通过 Ref 调用 :
html<ChildComponent ref="childRef" /> <button @click="handleCallChild">调用子组件</button>typescriptconst handleCallChild = () => { console.log(childRef.value?.name); // 输出:neon childRef.value?.open(); // 输出:执行 open 方法 };
✅
InstanceType<typeof Component>:获取组件的实例类型,确保调用安全。
3. 典型应用场景
常见于 UI 组件库(如 Element Plus、Vuetify)中的表单、弹窗等组件:例如表单组件通过 defineExpose 暴露 validate()(验证)、reset()(重置)等方法,父组件通过 ref 调用这些方法实现表单操作。
表单验证(Form 组件)
typescript
// 子组件:Form.vue
defineExpose({
validate: (callback: (valid: boolean) => void) => { /* 验证逻辑 */ },
reset: () => { /* 重置表单 */ }
})
typescript
// 父组件
const formRef = ref<InstanceType<typeof Form>>()
const submit = () => {
formRef.value?.validate((valid) => {
if (valid) {
console.log('表单验证通过,提交数据')
}
})
}
弹窗控制(Modal / Dialog)
typescript
// 子组件
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')实现。
- 基于