Vue中defineProps、defineEmits、defineExpose使用深度解析
前言
本文深度解析Vue3 <script setup> 語法中的三個核心編譯器宏:defineProps、defineEmits 和 defineExpose。
defineProps 用于定義組件屬性,支持多種驗證方式和TypeScript類型聲明;
defineEmits 用于聲明組件事件,可與v-model集成;
defineExpose 則用于暴露組件實例的公共API。
文章詳細(xì)介紹了每個宏的基本用法、TypeScript支持、最佳實踐以及常見問題解答,并展示了三者如何配合使用構(gòu)建完整組件。
這些宏必須在<script setup>頂層使用,具有編譯時常量限制,但能顯著提升代碼簡潔性和類型安全性。
這三個都是 Vue 3 <script setup> 語法中的編譯器宏(編譯時處理的特殊函數(shù))。它們不需要導(dǎo)入,直接在 <script setup> 中使用。
1.defineProps- 定義組件屬性
基本用法
<script setup>
// 方式1:數(shù)組形式(簡單,無類型檢查)
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:類型聲明 + 默認(rèn)值(Vue 3.3+)
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
})
// 方式3:復(fù)雜類型(聯(lián)合類型、自定義類型)
type Status = 'loading' | 'success' | 'error'
interface ComplexProps {
id: number | string // 聯(lián)合類型
status: Status // 自定義類型
metadata?: Record<string, any>
}
const props = defineProps<ComplexProps>()
</script>響應(yīng)式處理 Props
<script setup>
import { computed, toRef, toRefs } from 'vue'
const props = defineProps({
user: Object,
active: Boolean
})
// ? 錯誤:直接解構(gòu)會丟失響應(yīng)式
const { user, active } = props
// ? 正確:使用 toRefs 保持響應(yīng)式
const { user, active } = toRefs(props)
// ? 使用 computed 派生值
const userName = computed(() => props.user?.name || 'Unknown')
// ? 使用 toRef 處理單個 prop(props 可能沒有該屬性)
const userId = toRef(props, 'id') // 安全訪問,有默認(rèn)值
</script>2.defineEmits- 定義組件事件
基本用法
<script setup>
// 方式1:數(shù)組形式(簡單)
const emit = defineEmits(['submit', 'update:value'])
// 方式2:對象形式(支持驗證)
const emit = defineEmits({
// 無驗證
submit: null,
// 帶驗證函數(shù)
'update:value': (value) => {
if (typeof value === 'string' && value.length > 0) {
return true
}
console.warn('Invalid value')
return false
},
// 多個參數(shù)的驗證
'form-submit': (data, timestamp) => {
return data && typeof timestamp === 'number'
}
})
// 觸發(fā)事件
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- 暴露組件實例
為什么需要?
默認(rèn)情況下,<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,
// 甚至暴露函數(shù)
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() // ? 可以調(diào)用
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>()
// 現(xiàn)在有完整的類型提示
childRef.value?.increment() // ? 類型安全
</script>三者的關(guān)系與配合使用
完整組件示例
<!-- 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. 組件內(nèi)部狀態(tài)
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. 生命周期/監(jiān)聽
watch(() => props.userId, fetchUser, { immediate: true })
</script>
<template>
<!-- 模板內(nèi)容 -->
</template>注意事項和最佳實踐
1.執(zhí)行時機
// 這些編譯器宏必須在 <script setup> 的頂層作用域中使用
// ? 錯誤:不能在函數(shù)內(nèi)使用
function setupProps() {
defineProps({}) // 編譯錯誤
}
// ? 正確:頂層使用
defineProps({})2.重復(fù)定義
// ? 錯誤:不能多次調(diào)用
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>
// 不能動態(tài)生成
const propName = 'title'
// ? 錯誤:參數(shù)必須是編譯時常量
defineProps({ [propName]: String })5.最佳實踐總結(jié)
| 宏 | 最佳實踐 |
|---|---|
| defineProps | 1. 始終添加類型驗證 2. 為可選屬性設(shè)置默認(rèn)值 3. 使用 TypeScript 泛型獲得更好類型安全 4. 避免在子組件中修改 props |
| defineEmits | 1. 事件名使用 kebab-case 2. 為復(fù)雜事件添加驗證函數(shù) 3. 使用 TypeScript 定義完整事件簽名 4. 傳遞最小必要數(shù)據(jù) |
| defineExpose | 1. 僅暴露必要的 API 2. 為暴露的 API 添加 TypeScript 接口 3. 避免暴露內(nèi)部狀態(tài) 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 會暴露所有內(nèi)容嗎?
// 不會!defineExpose 是選擇性的
const publicData = ref('public')
const privateData = ref('private')
defineExpose({ publicData })
// 只有 publicData 被暴露,privateData 保持私有Q4: 可以在組合式函數(shù)中使用這些宏嗎?
// ? 錯誤:不能在組合式函數(shù)中使用
export function useFeature() {
defineProps({}) // 編譯錯誤
return {}
}
// ? 正確:只能在組件頂層使用這三個編譯器宏是 Vue 3 <script setup> 的核心,它們讓組件定義更加簡潔、類型安全,同時保持了良好的封裝性。掌握它們的使用是高效開發(fā) Vue 3 應(yīng)用的關(guān)鍵。
總結(jié)
到此這篇關(guān)于Vue中defineProps、defineEmits、defineExpose使用的文章就介紹到這了,更多相關(guān)Vue defineProps、defineEmits、defineExpose內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何使用el-table+el-tree+el-select動態(tài)選擇對應(yīng)值
小編在做需求時,遇到了在el-table表格中加入多條數(shù)據(jù),并且每條數(shù)據(jù)要通過el-select來選取相應(yīng)的值,做到動態(tài)選擇,下面這篇文章主要給大家介紹了關(guān)于如何使用el-table+el-tree+el-select動態(tài)選擇對應(yīng)值的相關(guān)資料,需要的朋友可以參考下2023-01-01
Vue表單數(shù)據(jù)修改與刪除功能實現(xiàn)
本文通過實例代碼介紹了Vue表單數(shù)據(jù)修改與刪除功能實現(xiàn),結(jié)合實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友跟隨小編一起看看吧2023-10-10
vue如何調(diào)用攝像頭實現(xiàn)拍照上傳圖片、本地上傳圖片
這篇文章主要給大家介紹了關(guān)于vue如何調(diào)用攝像頭實現(xiàn)拍照上傳圖片、本地上傳圖片的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2023-07-07
vue項目中應(yīng)用ueditor自定義上傳按鈕功能
這篇文章主要介紹了vue項目中應(yīng)用ueditor自定義上傳按鈕功能,文中以vue-cli生成的項目為例給大家介紹了vue項目中使用ueditor的方法,感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-04-04

