likes
comments
collection
share

组件v-model

作者站长头像
站长
· 阅读数 10

v-model是vue里面非常重要的一个内置指令,作用是在表单输入元素或者组件上创建双向绑定,这些元素仅包含:

  • <input>
  • <select>
  • <textarea>
  • components

v-model其实就是value属性和input事件的语法糖

<input type="text" :value="iptVal" @input="$event => iptVal = $event.target.value" />
<!-- v-model -->
<input type="text" v-model="iptVal" />

我们在表单开发时通常会使用v-model指令来完成数据绑定,组件components上同样可以使用该指令。

基本使用

v-model 可以在组件上使用以实现双向绑定。上面的示例是v-mdoel在表单元素上的语法糖使用,而当在一个组件上使用时,它其实是modelValue属性和update:modelValue事件的语法糖

<IptCpn
  :modelValue="searchText"
  @update:modelValue="newValue => searchText = newValue"
/>
<!-- v-model -->
<IptCpn v-model="searchText" />

在组件内部需要做两件事来实现功能,参见官方文档的两句话:

  1. 将内部原生 <input> 元素的 value attribute 绑定到 modelValue prop
  2. 当原生的 input 事件触发时,触发一个携带了新值的 update:modelValue 自定义事件

所以子组件代码为:

<!-- IptCpn.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
  <input
    type="text"
    :value="modelValue"
    @input="$event => $emit('update:modelValue', $event.target.value)"
  />
</template>

这时组件v-model就可以工作了,但是子组件中的元素还是必须绑定value属性和监听input事件,如果子组件中的表单元素也想使用语法糖写法v-model来绑定状态,需要使用拥有getter 和 setter 的computed来编写:

<!-- IptCpn.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const modelValueComputed = computed({
  set(val) {
    emit('update:modelValue', val)
  },
  get() {
    return props.modelValue
  }
})
</script>

<template>
  <input
    type="text"
    v-model="modelValueComputed"
  />
</template>

修饰符

v-model 有一些内置的修饰符,例如 .trim.number.lazy。这些在组件v-model是同样可以使用的,并且支持自定义修饰符来扩展一些功能

<IptCpn v-model.trim="searchText" />

多个v-model

组件上的v-model可以使用多个,使用v-model传参(就是给v-model一个名字)

<!-- 父 -->
<IptCpn v-model="prop1" v-model:title="prop2"/>

<!-- IptCpn.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps(['modelValue', 'title'])
const emit = defineEmits(['update:modelValue', 'update:title'])

const modelValueComputed = computed({
  set(val) {
    emit('update:modelValue', val)
  },
  get() {
    return props.modelValue
  }
})
const titleComputed = computed({
  set(val) {
    emit('update:title', val)
  },
  get() {
    return props.title
  }
})
</script>

<template>
  <input
    type="text"
    v-model="modelValueComputed"
  />
  <input
    type="text"
    v-model="titleComputed"
  />
</template>

这样就可以在单个组件实例上创建多个 v-model 双向绑定,但是这种写法冗余代码太多,每增加一个v-model绑定就会多写一个computed,如果表单元素过多,那么代码量就会增加很多。

优化

我们来思考一下,子组件中的多个表单元素绑定的数据可以看成是一类状态,我们将这一类状态放进一个对象中再使用v-model绑定到组件中:

<IptCpn v-model="formData" />

这里有一个需要注意的地方,我们现在再输入框中输入值,改变的是对象里面的某个字段的值,而不是这个对象,所以不会进入到setter当中,我们可以使用Proxy来代理整个对象,当每个值发生更改时,都做一次emit

<script setup>
import { computed } from "vue"
const props = defineProps({
  modelValue: {
    type: Object,
    require: true,
  },
})
const emit = defineEmits(["update:modelValue"])

const modelValueComputed = computed({
  set(val) {
    // ❌
    emit("update:modelValue", val)
  },
  get() {
    //✅
    return new Proxy(props.modelValue, {
      set(target, name, value) {
        target[name] = value
        emit("update:modelValue", target)
        return true
      },
    })
  },
})
</script>

<template>
  <input type="text" placeholder="姓名" v-model="modelValueComputed.name" />
  <input type="text" placeholder="年龄" v-model="modelValueComputed.age" />
</template>

在实际开发中,还可以将该部分代码抽成一个hook,使其成为一个通用的方法,以便使用。