前言
在 Vue 3.4 中,defineModel
宏的引入标志着 Vue 双向绑定机制的一次重大革新。作为 Composition API 的重要补充,defineModel
不仅简化了代码结构,还显著提升了开发效率和代码可维护性。本文将深入探讨 defineModel
的核心原理、最佳实践以及在实际项目中的应用场景,展示其如何优雅地解决传统 v-model
实现中的痛点。
传统双向绑定的痛点
在 defineModel
出现之前,Vue 的双向绑定主要依赖于 v-model
和手动管理 props
和 emits
。虽然这些方法有效,但在复杂场景下,代码往往显得冗长且难以维护。
方案一:手动管理 props
和 emits
- 父组件传递数据的同时需要实现一个修改数据的方法传递给子组件
vue
<!-- 父组件 -->
<child :carObj="carObj" @carPriceAdd="carPriceAdd" />
<script setup lang="ts">
const carObj = ref<ICarObj>({
brand: 'BMW',
price: 100000
})
const carPriceAdd = () => {
carObj.value.price += 1000
}
</script>
- 子组件接收数据的同时还需要接收父组件传递过来的事件,并通过
emits
触发调用,就可以修改父组件的数据了
vue
<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
,可以省去父组件定义修改数据的方法并传递给子组件这一步
- 父组件通过
v-model
传递数据给子组件
vue
<child v-model="user" />
<script setup lang="ts">
const user = ref<IUser>({
name: 'song',
age: 18
})
</script>
- 子组件在接受数据的同时也还要接受事件,只不过这个事件并不是父组件显式传递过来的,并且格式有点区别
vue
<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
默认传递过来的参数名为:modelValue ,默认传递过来的事件为:update:modelValue -
默认参数名在父组件中可以修改,格式为:
v-model:name
,同理子组件中接受的数据名与事件名改成一致即可
尽管 v-model
简化了部分代码,但仍需手动管理 props
和 emits
,尤其是在处理多个双向绑定时,代码复杂度显著增加。所以从 Vue 3.4 开始,官方更加推荐使用 defineModel()
宏来实现双向数据绑定。
defineModel
的诞生:简化双向绑定
defineModel 是一个编译器宏,用于在 Vue 组件中定义双向绑定的 prop。它本质上是对 v-model 指令的语法糖,但提供了更简洁、更直观的语法。
基本用法
父组件还是不变,只需通过v-model
传递数据给子组件即可
vue
<child v-model="user" />
<script setup lang="ts">
const user = ref<IUser>({
name: 'song',
age: 18
})
</script>
通过 defineModel
,子组件无需再显式接收 props
和 emits
,直接通过 defineModel
返回的 ref
对象即可实现双向绑定。
vue
<script setup lang="ts">
// 通过defineModel声明父组件传递过来的数据,返回一个ref对象
const user = defineModel<IUser>('user', {
default: {}
})
// 子组件可以直接修改刚刚通过defineModel声明的数据,不需要通过emits,父组件会自动更新
const ageAdd = () => {
user.value.age += 1
}
</script>
修饰符与转换器
在一些特殊场景下,我们可能还需要使用v-model
的修饰符功能
比如:清除字符串末尾的空格
父组件添加修饰符
vue
<!-- 父组件 -->
<child v-model:userName.trim="userName" />
子组件获取修饰符
在子组件中,我们可以通过解构 defineModel()
的返回值,来获取父组件添加到子组件 v-model
的修饰符:
js
// 通过defineModel声明父组件传递过来的数据,返回一个ref对象
const [user, filters] = defineModel<IUser>({
default: {},
set: (val) => {
console.log('set', val)
}
})
修饰符格式
默认格式为:第一个参数为props值,第二个参数为对应的修饰符(修饰符可能有多个,格式如下)

转换器处理数据
当存在修饰符时,我们可能需要在读取或将其同步回父组件时对其值进行转换。我们可以通过使用 get
和 set
转换器选项来实现这一点:
js
const [userName, userNameFilters] = defineModel('userName',{
default: '',
set: (val) => {
if(userNameFilters.trim) {
return val.trim()
}
return val
}
})
多Model
我们可以在单个组件实例上创建多个v-model
的双向绑定
比如:
vue
<!-- 父组件 -->
<child v-model.trim="user" v-model:userName.trim.number="userName" />
子组件同时接受多个v-model
js
// 通过defineModel声明父组件传递过来的数据,返回一个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
}
})
实现原理:defineModel
的背后
了解了怎么用的,最后再来看看它是怎么实现的
我们知道defineModel
其实就是v-model
的语法糖,所以我们可以对比下两种写法最后的编译结果有什么区别?
不使用defineModel

最终就是props
与emits
分别接收变量与事件
使用defineModel

使用defineModel
后,我们在组件中虽然可以不用像之前那样显式的接收props与emits,但Vue同样会帮我们生成这两块内容,并且可以看到两者红框内基本一样,只不过使用defineModel
会多一个修饰符的接收
defineModel
会被编译成一个 _useModel
方法,这是实现双向绑定的核心。从编译后的代码可以看出,defineModel
会接收父组件传递的 props
和 emits
,并利用 props
中的值进行初始化。当数据需要更新时,它会调用 emits
中注册的事件来通知父组件。然而,在实际开发中,我们通常不会直接操作 props
和 emits
,而是通过 defineModel
返回的 ref
值来直接操作数据。因此,_useModel
的核心任务是确保这个 ref
值与父组件传递的 props
值保持同步,从而实现数据的双向绑定。
结语:defineModel
的未来
defineModel
的引入不仅简化了 Vue 中的双向绑定,还为开发者提供了更强大的工具来处理复杂的数据流。随着 Vue 生态的不断发展,defineModel
必将在更多场景中发挥其重要作用,成为 Vue 开发者的得力助手。
通过本文的深入探讨,相信你已经对 defineModel
有了更全面的理解。在实际项目中,不妨尝试使用 defineModel
,体验其带来的便利与高效。