如何使用Vue 3.4的defineModel双向绑定,有什么优势?

前端 潘老师 2周前 (04-09) 16 ℃ (0) 扫码查看

Vue双向绑定让数据在组件间的传递和更新变得更加高效,Vue 3.4引入的defineModel宏,更是给双向绑定带来了全新的体验。今天,咱们就深入探究一下defineModel到底是什么,它有哪些优势,以及在实际项目中该怎么用。

一、传统双向绑定

defineModel出现之前,Vue实现双向绑定主要依靠v-model,以及手动管理propsemits。虽说这些方法能完成任务,但在稍微复杂点的场景下,就会暴露出不少问题。

(一)手动管理props和emits的麻烦

先看看手动管理propsemits的情况。在父组件中,不仅要传递数据,还得专门写一个修改数据的方法并传给子组件。比如下面这段代码:

<!-- 父组件 -->
<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的形式,子组件里接收的数据名和事件名也得跟着改。即便如此,在处理多个双向绑定时,手动管理propsemits的问题依然存在,代码复杂度还是降不下来。

二、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后,就不用再像以前那样显式接收propsemits了。直接通过defineModel返回的ref对象,就能轻松实现双向绑定,代码如下:

<script setup lang="ts">
// 通过defineModel声明父组件传递过来的数据,返回一个ref对象
const user = defineModel<IUser>('user', {
    default: {}
})
// 子组件可以直接修改刚刚通过defineModel声明的数据,不需要通过emits,父组件会自动更新
const ageAdd = () => {
    user.value.age += 1
}
</script>

这样一来,代码简洁了许多,逻辑也更清晰,大大提升了开发效率。

(二)修饰符与转换器的运用

  1. 修饰符的获取与使用
    在一些特殊场景下,我们会用到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. 转换器处理数据
当有修饰符存在时,有时候我们需要对数据进行转换,比如在读取数据或者把数据同步回父组件的时候。这时候可以通过getset转换器选项来实现。看下面这段代码:

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时编译结果的差异。

  1. 不使用defineModel的编译结果:不使用defineModel时,最终就是props接收变量,emits接收事件。代码结构相对复杂,需要开发者手动管理的部分较多。
  2. 使用defineModel的编译结果:使用defineModel后,虽然在组件中不用显式接收propsemits,但Vue还是会帮我们生成这部分内容。而且,使用defineModel会多一个修饰符的接收。defineModel会被编译成一个_useModel方法,这是实现双向绑定的关键。它会接收父组件传递的propsemits,用props的值进行初始化。当数据有更新需求时,就调用emits里注册的事件通知父组件。在实际开发中,我们一般通过defineModel返回的ref值来操作数据,_useModel的主要作用就是保证这个ref值和父组件传递的props值保持同步,进而实现数据的双向绑定。

通过这篇文章,相信大家对defineModel已经有了比较全面的了解。在实际项目开发中,大家不妨试试defineModel,感受一下。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/front/17055.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】