组件 v-model
基本用法
v-model 可用于组件上,实现双向绑定。
从 Vue 3.4 开始,实现这一点的推荐方式是使用 defineModel() 宏:
vue
<script setup>
const model = defineModel()
function update() {
model.value++
}
</script>
<template>
<div>父组件绑定的 v-model 是:{{ model }}</div>
<button @click="update">递增</button>
</template>然后父组件可以使用 v-model 绑定一个值:
template
<Child v-model="countModel" />defineModel() 返回的值是一个 ref。除了它会作为父组件值与本地值之间的双向绑定外,它的访问和修改方式与其他 ref 相同:
- 它的
.value会与父组件v-model绑定的值同步; - 当它被子组件修改时,也会导致父组件绑定的值被更新。
这意味着你也可以使用 v-model 将这个 ref 绑定到原生 input 元素上,从而在提供相同 v-model 用法的同时,轻松封装原生 input 元素:
vue
<script setup>
const model = defineModel()
</script>
<template>
<input v-model="model" />
</template>内部原理
defineModel 是一个便捷宏。编译器会将其展开为以下内容:
- 一个名为
modelValue的 prop,本地 ref 的值会与之同步; - 一个名为
update:modelValue的事件,当本地 ref 的值发生变更时会触发该事件。
这就是在 3.4 之前,上面展示的子组件应当如何实现:
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>然后,父组件中的 v-model="foo" 会被编译为:
template
<Child
:modelValue="foo"
@update:modelValue="$event => (foo = $event)"
/>如你所见,这要啰嗦不少。不过,了解其底层发生了什么是很有帮助的。
由于 defineModel 声明了一个 prop,因此你可以通过向 defineModel 传入参数来声明底层 prop 的选项:
js
// 使 v-model 成为必需
const model = defineModel({ required: true })
// 提供默认值
const model = defineModel({ default: 0 })WARNING
如果你为 defineModel prop 设置了 default 值,而父组件又没有为这个 prop 提供任何值,就可能导致父子组件之间的不同步。下面的示例中,父组件的 myRef 是 undefined,而子组件的 model 是 1:
vue
<script setup>
const model = defineModel({ default: 1 })
</script>vue
<script setup>
const myRef = ref()
</script>
<template>
<Child v-model="myRef"></Child>
</template>v-model 参数
组件上的 v-model 也可以接收一个参数:
template
<MyComponent v-model:title="bookTitle" />在子组件中,我们可以通过向 defineModel() 传入字符串作为其第一个参数来支持对应的参数:
vue
<script setup>
const title = defineModel('title')
</script>
<template>
<input type="text" v-model="title" />
</template>如果还需要 prop 选项,则应将它们放在模型名称之后传入:
js
const title = defineModel('title', { required: true })3.4 之前的用法
vue
<script setup>
defineProps({
title: {
required: true
}
})
defineEmits(['update:title'])
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>多个 v-model 绑定
借助前面在 v-model 参数 中学到的将某个特定 prop 和事件作为目标的能力,我们现在可以在单个组件实例上创建多个 v-model 绑定。
每个 v-model 都会同步到不同的 prop,而无需在组件中额外配置选项:
template
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>vue
<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>3.4 之前的用法
vue
<script setup>
defineProps({
firstName: String,
lastName: String
})
defineEmits(['update:firstName', 'update:lastName'])
</script>
<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>处理 v-model 修饰符
在学习表单输入绑定时,我们看到 v-model 有一些内置修饰符——.trim、.number 和 .lazy。在某些情况下,你可能还希望自定义输入组件上的 v-model 支持自定义修饰符。
让我们创建一个自定义修饰符示例 capitalize,它会将 v-model 绑定提供的字符串的首字母大写:
template
<MyComponent v-model.capitalize="myText" />添加到组件 v-model 上的修饰符,可以通过解构 defineModel() 的返回值在子组件中访问,如下所示:
vue
<script setup>
const [model, modifiers] = defineModel()
console.log(modifiers) // { capitalize: true }
</script>
<template>
<input type="text" v-model="model" />
</template>为了根据修饰符有条件地调整值应如何读取/写入,我们可以向 defineModel() 传入 get 和 set 选项。这两个选项会接收 model ref 在获取/设置时的值,并应返回一个转换后的值。我们可以像这样使用 set 选项来实现 capitalize 修饰符:
vue
<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>3.4 之前的用法
vue
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])
function emitValue(e) {
let value = e.target.value
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
<template>
<input type="text" :value="props.modelValue" @input="emitValue" />
</template>带参数的 v-model 修饰符
下面是另一个使用不同参数的多个 v-model 并搭配修饰符的示例:
template
<UserName
v-model:first-name.capitalize="first"
v-model:last-name.uppercase="last"
/>vue
<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName')
const [lastName, lastNameModifiers] = defineModel('lastName')
console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { uppercase: true }
</script>3.4 之前的用法
vue
<script setup>
const props = defineProps({
firstName: String,
lastName: String,
firstNameModifiers: { default: () => ({}) },
lastNameModifiers: { default: () => ({}) }
})
defineEmits(['update:firstName', 'update:lastName'])
console.log(props.firstNameModifiers) // { capitalize: true }
console.log(props.lastNameModifiers) // { uppercase: true }
</script>