如何使用Vue 3.4的defineModel双向绑定,有什么优势?
Vue双向绑定让数据在组件间的传递和更新变得更加高效,Vue 3.4引入的defineModel
宏,更是给双向绑定带来了全新的体验。今天,咱们就深入探究一下defineModel
到底是什么,它有哪些优势,以及在实际项目中该怎么用。
一、传统双向绑定
在defineModel
出现之前,Vue实现双向绑定主要依靠v-model
,以及手动管理props
和emits
。虽说这些方法能完成任务,但在稍微复杂点的场景下,就会暴露出不少问题。
(一)手动管理props和emits的麻烦
先看看手动管理props
和emits
的情况。在父组件中,不仅要传递数据,还得专门写一个修改数据的方法并传给子组件。比如下面这段代码:
<!-- 父组件 --> <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
触发调用,才能修改父组件的数据,代码如下:
<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
把数据传递给子组件,比如:
<child v-model="user" /> <script setup lang="ts"> const user = ref<IUser>({ name: 'song', age: 18 }) </script>
但是在子组件中,还是得接收事件,而且这个事件不是父组件直接传递过来的,格式也有点特别。具体代码如下:
<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
的形式,子组件里接收的数据名和事件名也得跟着改。即便如此,在处理多个双向绑定时,手动管理props
和emits
的问题依然存在,代码复杂度还是降不下来。
二、defineModel:双向绑定简化
(一)基本使用方法
defineModel
是Vue 3.4引入的编译器宏,它本质上是v-model
的语法糖,不过语法更简洁、直观。在使用defineModel
时,父组件的写法和使用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()
的返回值,就能获取父组件添加的修饰符,代码是这样的:
// 通过defineModel声明父组件传递过来的数据,返回一个ref对象 const [user, filters] = defineModel<IUser>({ default: {}, set: (val) => { console.log('set', val) } })
修饰符的格式是:第一个参数是props
值,第二个参数是对应的修饰符(修饰符可能有多个)。
2. 转换器处理数据
当有修饰符存在时,有时候我们需要对数据进行转换,比如在读取数据或者把数据同步回父组件的时候。这时候可以通过get
和set
转换器选项来实现。看下面这段代码:
const [userName, userNameFilters] = defineModel('userName',{ default: '', set: (val) => { if(userNameFilters.trim) { return val.trim() } return val } })
在这个例子里,如果userNameFilters
中有trim
修饰符,就会对val
进行去除末尾空格的操作。
(三)多Model的实现
defineModel
还支持在单个组件实例上创建多个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
的用法后,我们来探究一下它的实现原理。其实,defineModel
就是v-model
的语法糖,我们可以对比一下使用和不使用defineModel
时编译结果的差异。
- 不使用defineModel的编译结果:不使用
defineModel
时,最终就是props
接收变量,emits
接收事件。代码结构相对复杂,需要开发者手动管理的部分较多。 - 使用defineModel的编译结果:使用
defineModel
后,虽然在组件中不用显式接收props
和emits
,但Vue还是会帮我们生成这部分内容。而且,使用defineModel
会多一个修饰符的接收。defineModel
会被编译成一个_useModel
方法,这是实现双向绑定的关键。它会接收父组件传递的props
和emits
,用props
的值进行初始化。当数据有更新需求时,就调用emits
里注册的事件通知父组件。在实际开发中,我们一般通过defineModel
返回的ref
值来操作数据,_useModel
的主要作用就是保证这个ref
值和父组件传递的props
值保持同步,进而实现数据的双向绑定。
通过这篇文章,相信大家对defineModel
已经有了比较全面的了解。在实际项目开发中,大家不妨试试defineModel
,感受一下。