本文深度解析Vue3
<script setup>语法中的三个核心编译器宏:defineProps、defineEmits和defineExpose。
defineProps用于定义组件属性,支持多种验证方式和TypeScript类型声明;
defineEmits用于声明组件事件,可与v-model集成;
defineExpose则用于暴露组件实例的公共API。
文章详细介绍了每个宏的基本用法、TypeScript支持、最佳实践以及常见问题解答,并展示了三者如何配合使用构建完整组件。
这些宏必须在
<script setup>顶层使用,具有编译时常量限制,但能显著提升代码简洁性和类型安全性。
defineProps、defineEmits、defineExpose 深度解析
这三个都是 Vue 3 <script setup> 语法中的编译器宏 (编译时处理的特殊函数)。它们不需要导入,直接在 <script setup> 中使用。
1. defineProps - 定义组件属性
基本用法
vue
javascript
<script setup>
// 方式1:数组形式(简单,无类型检查)
const props = defineProps(['title', 'count'])
// 方式2:对象形式(推荐,支持完整验证)
const props = defineProps({
title: {
type: String,
required: true,
validator: (value) => value.length > 0
},
count: {
type: Number,
default: 0,
validator: (value) => value >= 0
},
items: {
type: Array,
default: () => []
},
config: {
type: Object,
default: () => ({})
}
})
</script>
TypeScript 用法
vue
TypeScript
<script setup lang="ts">
// 方式1:纯类型声明(运行时无验证)
interface Props {
title: string
count?: number
items?: string[]
}
const props = defineProps<Props>()
// 方式2:类型声明 + 默认值(Vue 3.3+)
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
})
// 方式3:复杂类型(联合类型、自定义类型)
type Status = 'loading' | 'success' | 'error'
interface ComplexProps {
id: number | string // 联合类型
status: Status // 自定义类型
metadata?: Record<string, any>
}
const props = defineProps<ComplexProps>()
</script>
响应式处理 Props
vue
javascript
<script setup>
import { computed, toRef, toRefs } from 'vue'
const props = defineProps({
user: Object,
active: Boolean
})
// ❌ 错误:直接解构会丢失响应式
const { user, active } = props
// ✅ 正确:使用 toRefs 保持响应式
const { user, active } = toRefs(props)
// ✅ 使用 computed 派生值
const userName = computed(() => props.user?.name || 'Unknown')
// ✅ 使用 toRef 处理单个 prop(props 可能没有该属性)
const userId = toRef(props, 'id') // 安全访问,有默认值
</script>
2. defineEmits - 定义组件事件
基本用法
vue
javascript
<script setup>
// 方式1:数组形式(简单)
const emit = defineEmits(['submit', 'update:value'])
// 方式2:对象形式(支持验证)
const emit = defineEmits({
// 无验证
submit: null,
// 带验证函数
'update:value': (value) => {
if (typeof value === 'string' && value.length > 0) {
return true
}
console.warn('Invalid value')
return false
},
// 多个参数的验证
'form-submit': (data, timestamp) => {
return data && typeof timestamp === 'number'
}
})
// 触发事件
const handleSubmit = () => {
emit('submit', { id: 1, data: 'test' })
}
const updateValue = (value) => {
emit('update:value', value)
}
</script>
TypeScript 用法
javascript
<script setup lang="ts">
// 方式1:类型字面量
const emit = defineEmits<{
(e: 'submit', data: FormData): void
(e: 'update:value', value: string): void
(e: 'toggle'): void
}>()
// 方式2:使用接口(更清晰)
interface Emits {
(e: 'submit', data: FormData): void
(e: 'update:modelValue', value: any): void
(e: 'click', event: MouseEvent): void
}
const emit = defineEmits<Emits>()
// 使用示例
const handleClick = (event: MouseEvent) => {
emit('click', event)
}
</script>
与 v-model 集成
vue
javascript
<!-- CustomInput.vue -->
<script setup>
// 支持 v-model
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const updateValue = (event) => {
emit('update:modelValue', event.target.value)
}
</script>
<template>
<input :value="modelValue" @input="updateValue" />
</template>
<!-- 父组件使用 -->
<template>
<CustomInput v-model="username" />
</template>
多个 v-model(Vue 3.3+)
vue
javascript
<!-- UserForm.vue -->
<script setup>
// 多个 v-model
const props = defineProps({
firstName: String,
lastName: String,
age: Number
})
const emit = defineEmits([
'update:firstName',
'update:lastName',
'update:age'
])
</script>
<template>
<input
:value="firstName"
@input="emit('update:firstName', $event.target.value)"
/>
<input
:value="lastName"
@input="emit('update:lastName', $event.target.value)"
/>
</template>
<!-- 父组件使用 -->
<template>
<UserForm
v-model:firstName="first"
v-model:lastName="last"
v-model:age="userAge"
/>
</template>
3. defineExpose - 暴露组件实例
为什么需要?
默认情况下,<script setup> 中的变量是私有的 ,父组件无法访问。defineExpose 用于显式暴露组件的方法和属性。
基本用法
vue
javascript
<!-- ChildComponent.vue -->
<script setup>
import { ref, computed } from 'vue'
// 私有变量(父组件无法访问)
const privateCount = ref(0)
const internalData = ref('secret')
// 公共方法
const publicMethod = () => {
console.log('Public method called')
privateCount.value++
}
// 计算属性
const publicComputed = computed(() => privateCount.value * 2)
// 暴露给父组件
defineExpose({
publicMethod,
publicComputed,
// 也可以直接暴露 ref
count: privateCount,
// 甚至暴露函数
reset: () => { privateCount.value = 0 }
})
</script>
父组件使用
vue
javascript
<!-- ParentComponent.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = ref(null)
onMounted(() => {
// 访问暴露的属性和方法
if (childRef.value) {
childRef.value.publicMethod() // ✅ 可以调用
console.log(childRef.value.publicComputed) // ✅ 可以访问
console.log(childRef.value.count) // ✅ 可以访问(因为被暴露)
// ❌ 无法访问未暴露的
console.log(childRef.value.internalData) // undefined
console.log(childRef.value.privateCount) // undefined
}
})
</script>
<template>
<ChildComponent ref="childRef" />
</template>
TypeScript 类型支持
vue
javascript
<!-- ChildComponent.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const name = ref('Vue')
const increment = () => {
count.value++
}
// 定义暴露的类型
export interface ExposedAPI {
count: number
name: string
increment: () => void
}
defineExpose<ExposedAPI>({
count,
name,
increment
})
</script>
<!-- ParentComponent.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import ChildComponent, { ExposedAPI } from './ChildComponent.vue'
const childRef = ref<ExposedAPI>()
// 现在有完整的类型提示
childRef.value?.increment() // ✅ 类型安全
</script>
三者的关系与配合使用
完整组件示例
vue
javascript
<!-- UserProfile.vue -->
<script setup>
import { ref, computed, watch } from 'vue'
// 1. 定义 Props
const props = defineProps({
userId: {
type: [String, Number], // 支持多种类型
required: true
},
editable: {
type: Boolean,
default: false
}
})
// 2. 定义 Emits
const emit = defineEmits({
'update:user': (userData) => {
return userData && typeof userData.id === 'number'
},
'save': null,
'cancel': null
})
// 3. 组件内部状态
const userData = ref(null)
const loading = ref(false)
const error = ref(null)
// 4. 方法
const fetchUser = async () => {
loading.value = true
try {
const response = await fetch(`/api/users/${props.userId}`)
userData.value = await response.json()
emit('update:user', userData.value)
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
const saveChanges = async () => {
// 保存逻辑...
emit('save', userData.value)
}
// 5. 暴露给父组件的方法
defineExpose({
refresh: fetchUser,
reset: () => {
userData.value = null
error.value = null
}
})
// 6. 生命周期/监听
watch(() => props.userId, fetchUser, { immediate: true })
</script>
<template>
<!-- 模板内容 -->
</template>
注意事项和最佳实践
1. 执行时机
javascript
// 这些编译器宏必须在 <script setup> 的顶层作用域中使用
// ❌ 错误:不能在函数内使用
function setupProps() {
defineProps({}) // 编译错误
}
// ✅ 正确:顶层使用
defineProps({})
2. 重复定义
javascript
// ❌ 错误:不能多次调用
defineProps({ title: String })
defineProps({ count: Number }) // 编译错误
// ✅ 正确:一次定义所有
defineProps({
title: String,
count: Number
})
3. 与 Options API 混用
vue
javascript
<script>
// 可以在同一个组件中与 Options API 混用
export default {
// Options API
inheritAttrs: false,
// 自定义选项
customOption: 'value'
}
</script>
<script setup>
// Composition API
const props = defineProps({/* ... */})
</script>
4. 使用限制
javascript
// 不能用在普通 <script> 中
<script>
// ❌ 错误:不能在普通 script 中使用
defineProps({}) // 编译错误
</script>
// 不能动态生成
const propName = 'title'
// ❌ 错误:参数必须是编译时常量
defineProps({ [propName]: String })
5. 最佳实践总结
| 宏 | 最佳实践 |
|---|---|
defineProps |
1. 始终添加类型验证 2. 为可选属性设置默认值 3. 使用 TypeScript 泛型获得更好类型安全 4. 避免在子组件中修改 props |
defineEmits |
1. 事件名使用 kebab-case 2. 为复杂事件添加验证函数 3. 使用 TypeScript 定义完整事件签名 4. 传递最小必要数据 |
defineExpose |
1. 仅暴露必要的 API 2. 为暴露的 API 添加 TypeScript 接口 3. 避免暴露内部状态 4. 提供清晰的公共方法名 |
常见问题解答
Q1: 为什么需要 .value 访问 props?
javascript
const props = defineProps({ count: Number })
// ❌ 错误:props 本身不是 ref
props.count.value // undefined
// ✅ 正确:直接访问
console.log(props.count)
// ✅ 如果需要 ref,使用 toRef
import { toRef } from 'vue'
const countRef = toRef(props, 'count')
Q2: 如何访问未定义的 props?
javascript
const props = defineProps({ definedProp: String })
// 安全访问未定义的 prop
import { toRef } from 'vue'
const optionalProp = toRef(props, 'optionalProp') // 返回一个 ref,即使 prop 未定义
Q3: defineExpose 会暴露所有内容吗?
javascript
// 不会!defineExpose 是选择性的
const publicData = ref('public')
const privateData = ref('private')
defineExpose({ publicData })
// 只有 publicData 被暴露,privateData 保持私有
Q4: 可以在组合式函数中使用这些宏吗?
javascript
// ❌ 错误:不能在组合式函数中使用
export function useFeature() {
defineProps({}) // 编译错误
return {}
}
// ✅ 正确:只能在组件顶层使用
这三个编译器宏是 Vue 3 <script setup> 的核心,它们让组件定义更加简洁、类型安全,同时保持了良好的封装性。掌握它们的使用是高效开发 Vue 3 应用的关键。