Code前端首页关于Code前端联系我们

Vue3组件双向绑定难写?useModel帮你省掉30%重复代码

terry 10小时前 阅读数 158 #Vue

你是不是也在Vue3开发里头疼过双向绑定的实现?比如封装一个自定义输入框组件,要给父组件传个prop叫modelValue,子组件里不能直接改它,得写emit('update:modelValue'),有时候还要加个computed的get和set层才能用v-model?遇到这种重复工作写多了,手都酸了,代码还不整洁,别慌,Vue3.2正式推出的内置组合式API useModel,就是专门解决这个问题的。

先别急着用!先搞懂Vue3双向绑定的“老套路”是什么

在说useModel之前,得先回忆下没它的时候怎么玩——很多新手甚至老手都会踩这个“老套路”的坑,搞懂原理才能真正用好useModel。

原来的双向绑定分两种情况:默认v-model和带自定义名称的v-model(比如v-model:checked),先拿最简单的封装单行文本输入框举例默认情况:

  1. 父组件里给子组件写<MyInput v-model="username" />,相当于传了两个东西:prop名为modelValue,值是username;还有个自动生成的事件监听@update:modelValue,当子组件触发这个事件传参时,父组件的username会自动更新为传参值。
  2. 子组件里呢?不能直接对modelValue赋值哦,会有警告甚至报错,因为Vue强调“单向数据流”,子组件只能用父组件传的prop,修改的话必须通过事件通知父组件改源数据,所以子组件里得先定义props和emits,再写个computed的双向属性绑定到input上:
    // MyInput.vue (没useModel的版本)
    <script setup>
    const props = defineProps({
    modelValue: {
     type: String,
     default: ''
    }
    })
    const emits = defineEmits(['update:modelValue'])
    const inputValue = computed({
    get() { return props.modelValue },
    set(val) { emits('update:modelValue', val) }
    })
    </script>
``` 如果是带自定义名称的,比如封装个开关组件,父组件用``,那子组件的prop就不能叫modelValue了,得改成`isOn`,事件监听变成`@update:isOn`,computed的get和set也得对应调整,要是一个组件里有好几个双向绑定(比如带搜索的表格:v-model="currentPage" v-model:searchKey="keyword"),那得定义好几个props、好几个emits、好几个computed,代码量蹭蹭涨,还特别容易写错prop或emit的名字。

那useModel到底是什么?能简化多少?

useModel是Vue3.2在组合式API里新增的内置工具函数,说白了就是帮你自动生成那个重复写烂的computed双向属性层,连props和emits的定义提示都能帮你省点事儿(如果你用TypeScript开发的话,类型推导还更爽)。

还是刚才那个单行文本输入框的例子,用上useModel后,代码直接砍一半都不止:

// MyInput.vue (用useModel的版本)
<script setup>
import { useModel } from 'vue'
const inputValue = useModel(props, 'modelValue')
</script>

等等,props和emits怎么没写?哦,Vue3.4之后对useModel做了进一步优化,如果只处理默认v-model(第一个参数留props占位但不用显式defineProps,或者显式定义但更简单),连defineProps和defineEmits都不用手动写!但为了代码可读性和类型安全(如果不用TypeScript可以显式加default),还是建议显式写一下简化版的props:

// 显式加props但更清晰的版本
<script setup>
import { useModel } from 'vue'
// 显式定义props,但不用重复defineEmits默认的update:xxx
const props = defineProps(['modelValue'])
const inputValue = useModel(props) // 第二个参数不传,默认就是'modelValue'
</script>
<template>
  <input type="text" v-model="inputValue" placeholder="请输入用户名" />
</template>

是不是简洁多了?父组件的写法完全没变,还是用<MyInput v-model="username" />,效果和原来一模一样。

如果是带自定义名称的,比如刚才的开关组件,父组件用<MySwitch v-model:isOn="lampStatus" />,用useModel的话就是把第二个参数改成自定义的名称'isOn'

// MySwitch.vue 用useModel的版本
<script setup>
import { useModel } from 'vue'
const props = defineProps(['isOn'])
const switchValue = useModel(props, 'isOn')
</script>
<template>
  <div 
    @click="switchValue = !switchValue" 
    class="switch"
    :class="{ on: switchValue }"
  >
    <div class="slider"></div>
  </div>
</template>
<style scoped>
/* 简单的开关样式,就不展开写了 */
</style>

这里的switchValue = !switchValue,其实就是内部自动触发了emits('update:isOn', !props.isOn),你完全不用管事件的事儿,直接把它当成普通的响应式变量赋值就行!

一个组件里多个双向绑定的场景更爽,比如带页码和搜索框的分页组件,父组件用<Pagination v-model="currentPage" v-model:searchKey="keyword" v-model:pageSize="size" />,用useModel的话就是写三次调用,每个对应一个自定义名称就行,代码依然很整洁:

// Pagination.vue 简化版多双向绑定
<script setup>
import { useModel } from 'vue'
const props = defineProps(['currentPage', 'searchKey', 'pageSize'])
const pageModel = useModel(props, 'currentPage')
const searchModel = useModel(props, 'searchKey')
const sizeModel = useModel(props, 'pageSize')
</script>
<template>
  <div class="pagination">
    <input type="text" v-model="searchModel" placeholder="搜索..." />
    <select v-model="sizeModel">
      <option value="10">10条/页</option>
      <option value="20">20条/页</option>
    </select>
    <!-- 这里可以省略页码按钮的逻辑,直接用pageModel就行 -->
    <span>当前第 {{ pageModel }} 页</span>
  </div>
</template>

对比一下没useModel的版本,你得写三个props、三个emits、三个computed,加起来少说20行,用了useModel之后,核心逻辑只有6行!说省30%重复代码都是保守估计,复杂一点的场景甚至能省50%以上。

useModel有没有什么“坑”?要注意哪些细节?

好用归好用,但还是有几个小细节要注意,不然可能会踩坑:

  1. 不要显式修改useModel返回的computed的.value属性!哦不对,不对,setup里如果用的是ref包装的响应式变量才需要.value,但useModel返回的是一个Proxy对象?或者说在模板里可以直接用,在setup的JS逻辑里要不要加.value? 对,这里容易搞混:useModel返回的是一个ref吗?不,严格来说是一个“响应式引用对象”,它和ref很像,在setup的JavaScript逻辑里(比如函数里、console.log里)需要加.value,但在template里可以直接用,不用加,比如刚才的开关组件,如果你要在JS里写个console.log看值,就得写console.log(switchValue.value),直接赋值的话也是switchValue.value = !switchValue.value吗?哦等下,刚才我写的是switchValue = !switchValue?不对不对,刚才手滑写错了!刚才的MySwitch.vue模板里用的是class="{ on: switchValue }"没问题,但@click里如果要在template里直接赋值的话,可以不用加.value,但如果是在setup里单独写个函数,

    // setup里的函数必须加.value
    const toggleSwitch = () => {
    switchValue.value = !switchValue.value
    }

    刚才的@click直接写switchValue = !switchValue是因为Vue的模板编译器会自动处理v-model或者这种直接赋值的响应式引用,给它加.value,所以没问题,但新手可能会在函数里忘记加,导致赋值无效,这是第一个小坑。

  2. 显式定义props的时候,不能有冲突! 比如你显式定义了modelValue的props,类型是String,但用useModel返回的对象赋值了Number类型,就会有警告,如果你在defineEmits里手动定义了update:modelValue或者自定义的update:xxx事件,Vue会自动合并,不会报错,但手动定义是多余的,useModel内部已经帮你处理好了。

  3. Vue3.2之前的版本用不了useModel! 这个不用多说,要是项目用的Vue3.0或者3.1,要么升级到3.2及以上(推荐升级,3.2之后性能提升很大,还多了很多好用的API),要么继续用老套路,或者可以自己封装一个类似useModel的工具函数,其实原理很简单,就是刚才老套路里的computed get和set,不过内置的useModel类型推导更完善,性能更好。

  4. 能不能给useModel返回的对象加额外的处理逻辑? 当然可以!比如你封装的输入框组件,要限制只能输入数字,原来的老套路是在computed的set里加判断,用useModel的话,不用直接改useModel返回的对象,而是可以再写一个computed或者watchEffect加逻辑:

    // 限制只能输入数字的MyInput.vue
    <script setup>
    import { useModel, computed } from 'vue'
    const props = defineProps(['modelValue'])
    const rawModel = useModel(props)
    // 加限制逻辑的computed
    const numberModel = computed({
    get() { return rawModel.value },
    set(val) {
     // 把非数字的字符替换掉
     const num = val.replace(/\D/g, '')
     rawModel.value = num
    }
    })
    </script>
``` 这里的rawModel就是useModel返回的原始对象,numberModel是加了限制逻辑的,这样既用了useModel的便利性,又加了自己的业务逻辑,一举两得。

useModel的原理是什么?知道原理能帮你解决更多问题

虽然知道怎么用就行,但了解一下原理能让你对Vue的双向绑定、单向数据流、组合式API有更深的理解,遇到复杂场景也能自己解决。

其实useModel的内部实现超级简单,就是刚才老套路里的computed加上defineModel的底层逻辑(哦对,Vue3.4还推出了defineModel宏,和useModel效果差不多,但写法更简洁,不过defineModel是实验性的API吗?不,Vue3.5之后defineModel已经稳定了,但useModel是更早推出的、更通用的API,不管你用不用TypeScript,不管你是在setup里还是在普通的组合式函数里,都能用)。

大概的内部实现思路是这样的:

  1. 接收两个参数:第一个是props对象(如果不传Vue3.4+会自动推断并生成),第二个是要绑定的prop名称(默认是'modelValue')。
  2. 检查是否已经定义了对应的props和emits,如果没有自动生成(Vue3.4+的功能)。
  3. 返回一个computed对象,get函数返回props[propName],set函数触发emits(update:${propName}, val)。

就这么简单!没有什么神秘的黑科技,就是把重复写的代码封装成了一个内置函数。

什么时候用useModel?

只要你封装的组件需要和父组件做双向数据绑定,就可以用useModel!不管是简单的输入框、开关、单选框、复选框,还是复杂的表格、分页器、日期选择器,都能用,它能帮你省掉大量重复的代码,让你的代码更整洁、更易读、更易维护。

赶紧去你的Vue3项目里试试吧!用了之后你一定会回来感谢我的。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门