更强大、更灵活! defineModel 重新定义双向绑定

前言

在 Vue 3.4 中,defineModel 宏的引入标志着 Vue 双向绑定机制的一次重大革新。作为 Composition API 的重要补充,defineModel 不仅简化了代码结构,还显著提升了开发效率和代码可维护性。本文将深入探讨 defineModel 的核心原理、最佳实践以及在实际项目中的应用场景,展示其如何优雅地解决传统 v-model 实现中的痛点。

传统双向绑定的痛点

defineModel 出现之前,Vue 的双向绑定主要依赖于 v-model 和手动管理 propsemits。虽然这些方法有效,但在复杂场景下,代码往往显得冗长且难以维护。

方案一:手动管理 propsemits

  1. 父组件传递数据的同时需要实现一个修改数据的方法传递给子组件
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>
  1. 子组件接收数据的同时还需要接收父组件传递过来的事件,并通过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,可以省去父组件定义修改数据的方法并传递给子组件这一步

  1. 父组件通过v-model传递数据给子组件
vue 复制代码
<child v-model="user" />
<script setup lang="ts">
const user = ref<IUser>({
    name: 'song',
    age: 18
})
</script>
  1. 子组件在接受数据的同时也还要接受事件,只不过这个事件并不是父组件显式传递过来的,并且格式有点区别
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 简化了部分代码,但仍需手动管理 propsemits,尤其是在处理多个双向绑定时,代码复杂度显著增加。所以从 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,子组件无需再显式接收 propsemits,直接通过 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值,第二个参数为对应的修饰符(修饰符可能有多个,格式如下)

转换器处理数据

当存在修饰符时,我们可能需要在读取或将其同步回父组件时对其值进行转换。我们可以通过使用 getset 转换器选项来实现这一点:

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

最终就是propsemits分别接收变量与事件

使用defineModel

使用defineModel后,我们在组件中虽然可以不用像之前那样显式的接收props与emits,但Vue同样会帮我们生成这两块内容,并且可以看到两者红框内基本一样,只不过使用defineModel会多一个修饰符的接收

defineModel 会被编译成一个 _useModel 方法,这是实现双向绑定的核心。从编译后的代码可以看出,defineModel 会接收父组件传递的 propsemits,并利用 props 中的值进行初始化。当数据需要更新时,它会调用 emits 中注册的事件来通知父组件。然而,在实际开发中,我们通常不会直接操作 propsemits,而是通过 defineModel 返回的 ref 值来直接操作数据。因此,_useModel 的核心任务是确保这个 ref 值与父组件传递的 props 值保持同步,从而实现数据的双向绑定。

结语:defineModel 的未来

defineModel 的引入不仅简化了 Vue 中的双向绑定,还为开发者提供了更强大的工具来处理复杂的数据流。随着 Vue 生态的不断发展,defineModel 必将在更多场景中发挥其重要作用,成为 Vue 开发者的得力助手。

通过本文的深入探讨,相信你已经对 defineModel 有了更全面的理解。在实际项目中,不妨尝试使用 defineModel,体验其带来的便利与高效。

相关推荐
bingbingyihao4 小时前
个人博客系统
前端·javascript·vue.js
BillKu6 小时前
遵守 Vue3 的单向数据流原则:父组件传递对象 + 子组件修改对象属性,安全地实现父子组件之间复杂对象的双向绑定示例代码及讲解
javascript·vue.js·elementui
涵信6 小时前
第八节:React HooksReact 18+新特性-React Server Components (RSC) 工作原理
前端·react.js·前端框架
Nuyoah.7 小时前
《vue3学习手记3》
前端·javascript·vue.js·学习·前端框架
初遇你时动了情7 小时前
vue3 uniapp vite 配置之定义指令
javascript·vue.js·uni-app
BillKu7 小时前
reactive 解构赋值给 ref
前端·javascript·vue.js
鑫不想说话1064768 小时前
ts+vue3出乎意料的推导报错
vue.js·typescript
dasseinzumtode8 小时前
在 Vue 3 中实现右键菜单功能
前端·vue.js
工业互联网专业9 小时前
基于springboot+vue的数码产品抢购系统
java·vue.js·spring boot·毕业设计·源码·课程设计·数码产品抢购系统
Data_Adventure9 小时前
使用CLINE快速生成一个3D展厅
vue.js·three.js·cline