IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    南玖发表于 2025-04-10 10:22:45
    love 0

    前言

    在 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>
    1. 子组件接收数据的同时还需要接收父组件传递过来的事件,并通过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>
    1. 子组件在接受数据的同时也还要接受事件,只不过这个事件并不是父组件显式传递过来的,并且格式有点区别
    <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分别接收变量与事件

    使用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,体验其带来的便利与高效。



沪ICP备19023445号-2号
友情链接