Vue中defineProps、defineEmits、defineExpose使用深度解析

 更新时间:2026年07月22日 10:59:43   作者:星汉灿烂星河  
在企业级 Vue3 项目中,组件通信是高频场景,也是最容易出现类型错误、传参错误、事件名错误的地方,<script setup> 提供了三个编译器宏,专门用于组件通信,这篇文章主要介绍了Vue中defineProps、defineEmits、defineExpose使用的相关资料,需要的朋友可以参考下

前言

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

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

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

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

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

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

这三个都是 Vue 3 <script setup> 语法中的编译器宏(编译时处理的特殊函数)。它们不需要导入,直接在 <script setup> 中使用。

1.defineProps- 定义组件属性

基本用法

<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 用法

<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

<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- 定义组件事件

基本用法

<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 用法

<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 集成

<!-- 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+)

<!-- 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 用于显式暴露组件的方法和属性。

基本用法

<!-- 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>

父组件使用

<!-- 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 类型支持

<!-- 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>

三者的关系与配合使用

完整组件示例

<!-- 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.执行时机

// 这些编译器宏必须在 <script setup> 的顶层作用域中使用
// ❌ 错误:不能在函数内使用
function setupProps() {
  defineProps({})  // 编译错误
}

// ✅ 正确:顶层使用
defineProps({})

2.重复定义

// ❌ 错误:不能多次调用
defineProps({ title: String })
defineProps({ count: Number })  // 编译错误

// ✅ 正确:一次定义所有
defineProps({
  title: String,
  count: Number
})

3.与 Options API 混用

vue

<script>
// 可以在同一个组件中与 Options API 混用
export default {
  // Options API
  inheritAttrs: false,
  
  // 自定义选项
  customOption: 'value'
}
</script>

<script setup>
// Composition API
const props = defineProps({/* ... */})
</script>

4.使用限制

// 不能用在普通 <script> 中
<script>
// ❌ 错误:不能在普通 script 中使用
defineProps({})  // 编译错误
</script>

// 不能动态生成
const propName = 'title'
// ❌ 错误:参数必须是编译时常量
defineProps({ [propName]: String })

5.最佳实践总结

最佳实践
defineProps1. 始终添加类型验证
2. 为可选属性设置默认值
3. 使用 TypeScript 泛型获得更好类型安全
4. 避免在子组件中修改 props
defineEmits1. 事件名使用 kebab-case
2. 为复杂事件添加验证函数
3. 使用 TypeScript 定义完整事件签名
4. 传递最小必要数据
defineExpose1. 仅暴露必要的 API
2. 为暴露的 API 添加 TypeScript 接口
3. 避免暴露内部状态
4. 提供清晰的公共方法名

常见问题解答

Q1: 为什么需要.value访问 props?

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?

const props = defineProps({ definedProp: String })

// 安全访问未定义的 prop
import { toRef } from 'vue'
const optionalProp = toRef(props, 'optionalProp')  // 返回一个 ref,即使 prop 未定义

Q3: defineExpose 会暴露所有内容吗?

// 不会!defineExpose 是选择性的
const publicData = ref('public')
const privateData = ref('private')

defineExpose({ publicData })
// 只有 publicData 被暴露,privateData 保持私有

Q4: 可以在组合式函数中使用这些宏吗?

// ❌ 错误:不能在组合式函数中使用
export function useFeature() {
  defineProps({})  // 编译错误
  return {}
}

// ✅ 正确:只能在组件顶层使用

这三个编译器宏是 Vue 3 <script setup> 的核心,它们让组件定义更加简洁、类型安全,同时保持了良好的封装性。掌握它们的使用是高效开发 Vue 3 应用的关键。

总结

到此这篇关于Vue中defineProps、defineEmits、defineExpose使用的文章就介绍到这了,更多相关Vue defineProps、defineEmits、defineExpose内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Nuxt pages下不同的页面对应layout下的页面布局操作

    Nuxt pages下不同的页面对应layout下的页面布局操作

    这篇文章主要介绍了Nuxt pages下不同的页面对应layout下的页面布局操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11
  • Vue Router的手写实现方法实现

    Vue Router的手写实现方法实现

    这篇文章主要介绍了Vue Router的手写实现方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-03-03
  • Vue3中#default=“scope”的使用

    Vue3中#default=“scope”的使用

    在Vue3中,我们经常会遇到需要处理列表渲染的情况,Vue3引入了一种新的语法,即#default=“scope”,本文就来介绍一下Vue3中#default=“scope”的使用,感兴趣的可以了解一下
    2023-10-10
  • Vue中Number方法将字符串转换为数字的过程

    Vue中Number方法将字符串转换为数字的过程

    这篇文章主要介绍了Vue中Number方法将字符串转换为数字,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-06-06
  • vue数组动态刷新失败问题及解决

    vue数组动态刷新失败问题及解决

    这篇文章主要介绍了vue数组动态刷新失败问题及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-03-03
  • Vue使用Canvas生成随机大小且不重叠圆

    Vue使用Canvas生成随机大小且不重叠圆

    Canvas是HTML5中新增的元素,专门用来绘制图形,下面这篇文章主要给大家介绍了关于Vue使用Canvas生成随机大小且不重叠圆的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2021-11-11
  • Vue Element前端应用开发之整合ABP框架的前端登录

    Vue Element前端应用开发之整合ABP框架的前端登录

    VUE+Element 前端是一个纯粹的前端处理,前面介绍了很多都是Vue+Element开发的基础,从本章随笔开始,就需要进入深水区了,需要结合ABP框架使用
    2021-05-05
  • vue css 引入asstes中的图片无法显示的四种解决方法

    vue css 引入asstes中的图片无法显示的四种解决方法

    这篇文章主要介绍了vue css 引入asstes中的图片 无法显示的几种解决方案,本文给出了四种解决方法,每种方法给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-03-03
  • 浅谈Vue插槽实现原理

    浅谈Vue插槽实现原理

    vue.js的灵魂是组件,而组件的灵魂是插槽。借助于插槽,我们能最大程度上实现组件复用。本文主要是对插槽的实现机制进行详细概括总结,在某些场景中,有一定的用处
    2021-06-06
  • Vue响应式原理及双向数据绑定示例分析

    Vue响应式原理及双向数据绑定示例分析

    这篇文章主要为大家介绍了Vue响应式原理及双向数据绑定的示例分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07

最新评论