Vue:defineProps、defineEmits、defineExpose 深度解析

本文深度解析Vue3 <script setup> 语法中的三个核心编译器宏:definePropsdefineEmitsdefineExpose


defineProps 用于定义组件属性,支持多种验证方式和TypeScript类型声明;


defineEmits 用于声明组件事件,可与v-model集成;


defineExpose 则用于暴露组件实例的公共API。


文章详细介绍了每个宏的基本用法、TypeScript支持、最佳实践以及常见问题解答,并展示了三者如何配合使用构建完整组件。


这些宏必须在<script setup>顶层使用,具有编译时常量限制,但能显著提升代码简洁性和类型安全性。


definePropsdefineEmitsdefineExpose 深度解析

这三个都是 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 应用的关键。

相关推荐
小徐不会敲代码~2 小时前
Vue3 学习 6
开发语言·前端·vue.js·学习
幽络源小助理2 小时前
SpringBoot+Vue多维分类知识管理系统源码 | Java知识库项目免费下载 – 幽络源
java·vue.js·spring boot
fengyucaihong_1232 小时前
vue加声音播放
javascript·vue.js·ecmascript
华仔啊2 小时前
Vue3 的设计目标是什么?相比 Vue2 做了哪些关键优化?
前端·vue.js
麦麦大数据2 小时前
F066 vue+flask中医草药靶点知识图谱智能问答系统|中医中药医学知识图谱
vue.js·flask·知识图谱·中医·草药·成分知识图谱·靶点
鹏多多2 小时前
前端纯js实现图片模糊和压缩
前端·javascript·vue.js
Aliex_git3 小时前
Vue 2 - 模板编译源码理解
前端·javascript·vue.js·笔记·前端框架
Irene19913 小时前
Vue:Props 和 Emits 对比总结
vue.js·props·emits
Irene19913 小时前
Vue 3 Composition API 中创建响应式数据的两个核心 API(ref 和 reactive)
vue.js·reactive·ref