v-model

在表单输入元素或者组件上创建双向绑定。

表单输入绑定

在前端处理表单时,我们常常需要将表单输入框的内容同步给代码中的变量。如果手动操作的话可能会比较麻烦。

<input
:value="text"
@input="event => text = event.target.value" />

v-model指令帮我们简化了这一操作:

<input v-model="text" />

v-model可以用于多种不同类型的输入,<textarea><select>元素。它会根据所使用的元素自动使用对应的DOM属性和事件结合。

  • 文本类型的<input><textarea>元素会绑定value property并监听input事件。
  • <input type="checkbox"><input type="radio">会绑定checked property并监听change事件。
  • <select>会绑定value property并监听change事件。

v-model 会忽略任何表单元素上初始的valuecheckedselected attribute。它始终将当前绑定的变量值视为数据的正确来源。

修饰符

.lazy

默认情况下,v-model会在每次input事件之后更新数据(IME拼字阶段的状态除外)。可以通过添加lazy修饰符来改为每次change事件后更新数据。

<!-- 在"change"事件后同步更新而不是"input" -->
<input v-model.lazy="msg" />

.number

如果想要在输入后自动将输入的内容转为数字,可以在v-model后添加.number修饰符来管理输入:

<input v-model.number="age" />

如果该值无法被parseFloat()函数处理,那么将返回原始值。number修饰符会在输入框有type="number"时自动启用。

.trim

如果想要默认自动去除用户输入内容两端的空格,可以在v-model后添加.trim修饰符:

<input v-model.trim="msg" />

组件v-model

v-model可以在组件上使用以实现双向绑定。从Vue3.4开始,推荐实现方式是使用defineModel()宏。

<!-- Child.vue -->
<script setup>
const model = defineModel()

function update() {
model.value++
}
</script>

<template>
<div>Parent bound v-model is: {{ model }}</div>
<button @click="update">Increment</button>
</template>

父组件可以用v-model绑定一个值:

<!-- Parent.vue -->
<Child v-model="countModel" />

defineModel()返回的值是一个ref,它可以像其他ref一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:

  • 它的.value和父组件的v-model的值同步。
  • 当它被子组件变更了,会触发父组件绑定的值一起更新。

这意味着也可以用v-model把这个ref绑定到一个原生input元素上,在提供相同v-model用法的同时轻松包装原生input元素。

底层机制

defineModel内部做了两件事:

  • 一个名为modelValue的prop,本地ref的值与其同步;
  • 一个名为update:modelValue的事件,当本地ref的值发生变更时触发。

举个例子:

<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

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

然后在父组件中

<!-- Parent.vue -->
<Child
:modelValue="foo"
@update:modelValue="$event => (foo = $event)"
/>

由此可见非常冗长,虽然这样写容易理解底层机制。

因为defineModel声明了一个prop,可以通过传递选项,来声明底层prop的选项:

// 使 v-model 必填
const model = defineModel({required: true})

// 提供一个默认值
const model = defineModel({default: 0})

注意点,如果给defineModel设置了一个默认值且在父组件中没有给该prop提供任何值,会导致父子组件之间不同步。

v-model的参数

组件上的v-model也可以接受一个参数:

<my-component v-model:title="bookTitle" />

在子组件中,可以通过将字符串作为第一个参数传递给defineModel()来支持相应的参数:

<!-- MyComponent.vue -->
<script setup>
const title = defineModel('title')
</script>

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

如果需要额外的prop选项,应该在model名称之后传递:

const title = defineModel('title', { required: true })

多个v-model绑定

利用v-model的参数可以指定参数的功能,可以在单个组件实例上创建多个v-model双向绑定。

组件上的每一个v-model都会同步不同的prop,而无需额外的选项:

<user-name
v-model:first-name="first"
v-model:last-name="last"
/>
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

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

处理v-model修饰符

在某些场景下,可能需要一个自定义组件的v-model支持自定义的修饰符。

比如,创建一个自定义的修饰符capitalize,它会自动将v-model绑定输入的字符串值第一个字母转为大写:

<my-component v-model.capitalize="myText" />

通过像这样解构defineModel()的返回值,可以在子组件访问添加到组件v-model的修饰符:

<script setup>
const [model, modifiers] = defineModel()

console.log(modifiers) // { capitalize: true }
</script>

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

为了能够基于修饰符选择性地调节值的读取和写入方式,我们可以给defineModel()传入getset这两个选项。这两个选项在从模型引用中读取或设置值时会接收到当前的值,并且它们都应该返回一个经过处理的新值。

<script setup>
const [model, modifiers] = defineModel({
set(value) {
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
return value
}
})
</script>

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