前言
在 Vue 3.4 中, defineModel
宏的引入标志着 Vue 双向绑定机制的一次重大革新。作为 Composition API的重要补充, defineModel
不仅简化了代码结构,还显著提升了开发效率和代码可维护性。本文将深入探讨 defineModel
的核心原理、最佳实践以及在实际项目中的应用场景,展示其如何优雅地解决传统 v-model
实现中的痛点。
传统双向绑定的痛点
在 defineModel
出现之前,Vue 的双向绑定主要依赖于 v-model
和手动管理 props
和 emits
。虽然这些方法有效,但在复杂场景下,代码往往显得冗长且难以维护。
方案一:手动管理 props
和 emits
1. 父组件传递数据的同时需要实现一个修改数据的方法传递给子组件
<!-- 父组件 -->
<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. 子组件接收数据的同时还需要接收父组件传递过来的事件,并通过 emits
调用,就可以修改父组件的数据了
<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
,可以省去父组件定义修改数据的方法并传递给子组件这一步
1. 父组件通过 v-model
传递数据给子组件
<child v-model="user" />
<script setup lang="ts">
const user = ref<IUser>({ name: 'song', age: 18
})
</script>
2. 子组件在接受数据的同时也还要接受事件,只不过这个事件并不是父组件显式传递过来的,并且格式有点区别
<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
传递数据给子组件即可
<child v-model="user" />
<script setup lang="ts">
const user = ref<IUser>({ name: 'song', age: 18
})
</script>
通过 defineModel
,子组件无需再显式接收 props
和 emits
,直接通过 defineModel
返回的 ref
对象即可实现双向绑定。
<script setup lang="ts">
// 通过defineModel声明父组件传递过来的数据,返回一个ref对象
const user = defineModel<IUser>('user', { default: {}
})
// 子组件可以直接修改刚刚通过defineModel声明的数据,不需要通过emits,父组件会自动更新
const ageAdd = () => { user.value.age += 1
}
</script>
修饰符与转换器
在一些特殊场景下,我们可能还需要使用 v-model
的修饰符功能
比如:清除字符串末尾的空格
** 父组件添加修饰符 **
<!-- 父组件 -->
<child v-model:userName.trim="userName" />
** 子组件获取修饰符 **
在子组件中,我们可以通过解构 defineModel()
的返回值,来获取父组件添加到子组件 v-model
的修饰符:
// 通过defineModel声明父组件传递过来的数据,返回一个ref对象
const [user, filters] = defineModel<IUser>({ default: {}, set: (val) => { console.log('set', val) }
})
** 修饰符格式 **
默认格式为:第一个参数为props值,第二个参数为对应的修饰符(修饰符可能有多个)
** 转换器处理数据 **
当存在修饰符时,我们可能需要在读取或将其同步回父组件时对其值进行转换。我们可以通过使用 get
和 set
转换器选项来实现这一点:
const [userName, userNameFilters] = defineModel('userName',{ default: '', set: (val) => { if(userNameFilters.trim) { return val.trim() } return val }
})
多Model
我们可以在单个组件实例上创建多个 v-model
的双向绑定
比如:
<!-- 父组件 -->
<child v-model.trim="user" v-model:userName.trim.number="userName" />
子组件同时接受多个 ` v-model
// 通过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,但Vue同样会帮我们生成这两块内容,并且可以看到两者红框内基本一样,只不过使用 defineModel
会多一个修饰符的接收defineModel
会被编译成一个 _useModel
方法,这是实现双向绑定的核心。从编译后的代码可以看出, defineModel
会接收父组件传递的 props
和 emits
,并利用 props
中的值进行初始化。当数据需要更新时,它会调用 emits
中注册的事件来通知父组件。然而,在实际开发中,我们通常不会直接操作 props
和 emits
,而是通过 defineModel
返回的 ref
值来直接操作数据。因此, _useModel
的核心任务是确保这个 ref
值与父组件传递的 props
值保持同步,从而实现数据的双向绑定。
结语: defineModel
的未来
defineModel
的引入不仅简化了 Vue 中的双向绑定,还为开发者提供了更强大的工具来处理复杂的数据流。随着 Vue 生态的不断发展,defineModel
必将在更多场景中发挥其重要作用,成为 Vue 开发者的得力助手。