更強大、更靈活! defineModel 重新定義雙向綁定
前言
在 Vue 3.4 中,defineModel 宏的引入標(biāo)志著 Vue 雙向綁定機制的一次重大革新。作為 Composition API 的重要補充,defineModel 不僅簡化了代碼結(jié)構(gòu),還顯著提升了開發(fā)效率和代碼可維護性。本文將深入探討 defineModel 的核心原理、最佳實踐以及在實際項目中的應(yīng)用場景,展示其如何優(yōu)雅地解決傳統(tǒng) v-model 實現(xiàn)中的痛點。
傳統(tǒng)雙向綁定的痛點
在 defineModel 出現(xiàn)之前,Vue 的雙向綁定主要依賴于 v-model 和手動管理 props 和 emits。雖然這些方法有效,但在復(fù)雜場景下,代碼往往顯得冗長且難以維護。
方案一:手動管理 props 和 emits
1. 父組件傳遞數(shù)據(jù)的同時需要實現(xiàn)一個修改數(shù)據(jù)的方法傳遞給子組件
<!-- 父組件 -->
<child :carObj="carObj" @carPriceAdd="carPriceAdd" />
<script setup lang="ts">
const carObj = ref<ICarObj>({
brand: 'BMW',
price: 100000
})
const carPriceAdd = () => {
carObj.value.price += 1000
}
</script>
2. 子組件接收數(shù)據(jù)的同時還需要接收父組件傳遞過來的事件,并通過emits調(diào)用,就可以修改父組件的數(shù)據(jù)了
<script setup lang="ts">
const props = defineProps<{
modelValue: IUser, // v-model
carObj: ICarObj // v-bind
}>()
const emits = defineEmits(['carPriceAdd'])
const priceAdd = () => {
emits('carPriceAdd')
console.log(props.carObj.price)
}
</script>
方案二:使用 v-model
還可以借助v-model,可以省去父組件定義修改數(shù)據(jù)的方法并傳遞給子組件這一步
1. 父組件通過v-model傳遞數(shù)據(jù)給子組件
<child v-model="user" />
<script setup lang="ts">
const user = ref<IUser>({
name: 'song',
age: 18
})
</script>
2. 子組件在接受數(shù)據(jù)的同時也還要接受事件,只不過這個事件并不是父組件顯式傳遞過來的,并且格式有點區(qū)別
<script setup lang="ts">
const props = defineProps<{
modelValue: IUser, // v-model
carObj: ICarObj // v-bind
}>()
const emits = defineEmits(['update:modelValue'])
const ageAdd = () => {
emits('update:modelValue', {
...props.modelValue,
age: props.modelValue.age + 1
})
// console.log(props.modelValue.age)
}
</script>
- v-model默認(rèn)傳遞過來的參數(shù)名為:modelValue,默認(rèn)傳遞過來的事件為:update:modelValue
- 默認(rèn)參數(shù)名在父組件中可以修改,格式為:v-model:name,同理子組件中接受的數(shù)據(jù)名與事件名改成一致即可
盡管 v-model 簡化了部分代碼,但仍需手動管理 props 和 emits,尤其是在處理多個雙向綁定時,代碼復(fù)雜度顯著增加。所以從 Vue 3.4 開始,官方更加推薦使用 defineModel() 宏來實現(xiàn)雙向數(shù)據(jù)綁定。
defineModel 的誕生:簡化雙向綁定
?
defineModel 是一個編譯器宏,用于在 Vue 組件中定義雙向綁定的 prop。它本質(zhì)上是對 v-model 指令的語法糖,但提供了更簡潔、更直觀的語法。
基本用法
父組件還是不變,只需通過v-model傳遞數(shù)據(jù)給子組件即可
<child v-model="user" />
<script setup lang="ts">
const user = ref<IUser>({
name: 'song',
age: 18
})
</script>
通過 defineModel,子組件無需再顯式接收 props 和 emits,直接通過 defineModel 返回的 ref 對象即可實現(xiàn)雙向綁定。
<script setup lang="ts">
// 通過defineModel聲明父組件傳遞過來的數(shù)據(jù),返回一個ref對象
const user = defineModel<IUser>('user', {
default: {}
})
// 子組件可以直接修改剛剛通過defineModel聲明的數(shù)據(jù),不需要通過emits,父組件會自動更新
const ageAdd = () => {
user.value.age += 1
}
</script>
修飾符與轉(zhuǎn)換器
在一些特殊場景下,我們可能還需要使用v-model的修飾符功能
比如:清除字符串末尾的空格
父組件添加修飾符
<!-- 父組件 -->
<child v-model:userName.trim="userName" />
子組件獲取修飾符
在子組件中,我們可以通過解構(gòu) defineModel() 的返回值,來獲取父組件添加到子組件 v-model 的修飾符:
// 通過defineModel聲明父組件傳遞過來的數(shù)據(jù),返回一個ref對象
const [user, filters] = defineModel<IUser>({
default: {},
set: (val) => {
console.log('set', val)
}
})
修飾符格式
默認(rèn)格式為:第一個參數(shù)為props值,第二個參數(shù)為對應(yīng)的修飾符(修飾符可能有多個,格式如下)
轉(zhuǎn)換器處理數(shù)據(jù)
當(dāng)存在修飾符時,我們可能需要在讀取或?qū)⑵渫交馗附M件時對其值進行轉(zhuǎn)換。我們可以通過使用 get 和 set 轉(zhuǎn)換器選項來實現(xiàn)這一點:
const [userName, userNameFilters] = defineModel('userName',{
default: '',
set: (val) => {
if(userNameFilters.trim) {
return val.trim()
}
return val
}
})
多Model
我們可以在單個組件實例上創(chuàng)建多個v-model的雙向綁定
比如:
<!-- 父組件 -->
<child v-model.trim="user" v-model:userName.trim.number="userName" />
子組件同時接受多個v-model
// 通過defineModel聲明父組件傳遞過來的數(shù)據(jù),返回一個ref對象
const [user, filters] = defineModel<IUser>({
default: {},
set: (val) => {
console.log('set', val)
}
})
const [userName, userNameFilters] = defineModel<string>('userName',{
default: '',
set: (val) => {
if(userNameFilters.trim) {
return val.trim()
}
return val
}
})
實現(xiàn)原理:defineModel 的背后
了解了怎么用的,最后再來看看它是怎么實現(xiàn)的
我們知道defineModel其實就是v-model的語法糖,所以我們可以對比下兩種寫法最后的編譯結(jié)果有什么區(qū)別?
不使用defineModel
圖片
最終就是props與emits分別接收變量與事件
使用defineModel
圖片
使用defineModel后,我們在組件中雖然可以不用像之前那樣顯式的接收props與emits,但Vue同樣會幫我們生成這兩塊內(nèi)容,并且可以看到兩者紅框內(nèi)基本一樣,只不過使用defineModel會多一個修飾符的接收
defineModel 會被編譯成一個 _useModel 方法,這是實現(xiàn)雙向綁定的核心。從編譯后的代碼可以看出,defineModel 會接收父組件傳遞的 props 和 emits,并利用 props 中的值進行初始化。當(dāng)數(shù)據(jù)需要更新時,它會調(diào)用 emits 中注冊的事件來通知父組件。然而,在實際開發(fā)中,我們通常不會直接操作 props 和 emits,而是通過 defineModel 返回的 ref 值來直接操作數(shù)據(jù)。因此,_useModel 的核心任務(wù)是確保這個 ref 值與父組件傳遞的 props 值保持同步,從而實現(xiàn)數(shù)據(jù)的雙向綁定。
結(jié)語:defineModel 的未來
defineModel 的引入不僅簡化了 Vue 中的雙向綁定,還為開發(fā)者提供了更強大的工具來處理復(fù)雜的數(shù)據(jù)流。隨著 Vue 生態(tài)的不斷發(fā)展,defineModel 必將在更多場景中發(fā)揮其重要作用,成為 Vue 開發(fā)者的得力助手。
通過本文的深入探討,相信你已經(jīng)對 defineModel 有了更全面的理解。在實際項目中,不妨嘗試使用 defineModel,體驗其帶來的便利與高效。