Vue3 组件 v-model
- [1. 基本使用](#1. 基本使用)
-
- [1.1 简单示例](#1.1 简单示例)
- [1.2 包装原生 DOM 元素](#1.2 包装原生 DOM 元素)
- [1.3 底层机制(Props + 自定义事件)](#1.3 底层机制(Props + 自定义事件))
- [1.4 支持简单验证](#1.4 支持简单验证)
-
- [1.4.1 支持必填校验](#1.4.1 支持必填校验)
- [1.4.2 提供一个默认值(不推荐使用,会有问题)](#1.4.2 提供一个默认值(不推荐使用,会有问题))
- [1.4.3 指定类型](#1.4.3 指定类型)
- [2. v-model 参数](#2. v-model 参数)
-
- [2.1 给 v-model 命名](#2.1 给 v-model 命名)
- [2.2. 多个 v-model 绑定](#2.2. 多个 v-model 绑定)
- [3. v-model 修饰符](#3. v-model 修饰符)
-
- [3.1 内置修饰符(表单修饰符)](#3.1 内置修饰符(表单修饰符))
- [3.2 自定义修饰符](#3.2 自定义修饰符)
-
- [3.2.1 使用方式(从 defineModel() 解构,并且在 set 中自定义修饰符行为)](#3.2.1 使用方式(从 defineModel() 解构,并且在 set 中自定义修饰符行为))
- [3.2.2 不能用于初始值的转换(需要的话,建议改用计算属性)](#3.2.2 不能用于初始值的转换(需要的话,建议改用计算属性))
- [3.3 带参数的修饰符(多个v-model 使用修饰符)](#3.3 带参数的修饰符(多个v-model 使用修饰符))
1. 基本使用
1.1 简单示例
v-model 可以在组件上使用以实现双向绑定。
从 Vue 3.4 开始,推荐的实现方式是使用 defineModel() 宏。
子组件 Child.vue:
javascript
<template>
<div>Parent bound v-model is: {{ model }}</div>
<button @click="update">Increment</button>
</template>
<script setup>
const model = defineModel()
function update() {
model.value++
}
</script>
父组件 App.vue:
javascript
<template>
<div>
<Child v-model="num"/>
</div>
</template>
<script setup>
import Child from '@/components/Child.vue'
import { ref, watch } from 'vue'
const num = ref(0)
watch(num, (newVal) => {
console.log('监听到了变化:', newVal)
})
</script>
<style lang="scss" scoped>
</style>

可以看到,子组件使用 defineModel 定义的数据,在父组件上,可以直接使用 v-model 进行双向数据绑定。
1.2 包装原生 DOM 元素
基于 1.1 中的知识,我们可以很轻松地对 DOM 元素,比如 input 进行二次包装。
子组件 MyInput:
javascript
<script setup>
const model = defineModel()
</script>
<template>
<input v-model="model" />
</template>
父组件 App.vue:
javascript
<template>
<div>
<h1>{{ msg }}</h1>
<MyInput v-model="msg"/>
</div>
</template>
<script setup>
import MyInput from '@/components/MyInput.vue'
import { ref } from 'vue'
const msg = ref('Hello World!')
</script>
<style lang="scss" scoped>
</style>

修改子组件内容,父组件数据同步修改。

1.3 底层机制(Props + 自定义事件)
defineModel 是一个便利宏。编译器将其展开为以下内容:
- 一个名为 modelValue 的 prop,本地 ref 的值与其同步;
- 一个名为 update:modelValue 的事件,当本地 ref 的值发生变更时触发。
在 3.4 版本前,我们通常会使用 Props 结合 自定义事件的方式,实现父子组件的双向数据绑定。比如:
子组件 MyInput.vue:
javascript
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
父组件 App.vue:
javascript
<template>
<h2>{{ msg }}</h2>
<MyInput :modelValue="msg" @update:modelValue="$event => (msg = $event)" />
</template>
<script setup>
import MyInput from '@/components/MyInput.vue'
import { ref } from 'vue'
const msg = ref('Hello World!')
</script>
<style lang="scss" scoped></style>
虽然这样写,代码会相对冗长,但是底层确实是这么实现的,这么写也有助于理解双向数据绑定的视线机制。
1.4 支持简单验证
1.4.1 支持必填校验
关键代码:
javascript
// 使 v-model 必填
const model = defineModel({ required: true })
子组件 MyInput.vue:
javascript
<script setup>
const model = defineModel({ required: true })
</script>
<template>
<input v-model="model" />
</template>
父组件 App.vue:
javascript
<template>
<h2>{{ msg }}</h2>
<MyInput />
</template>
<script setup>
import MyInput from '@/components/MyInput.vue'
import { ref} from 'vue'
const msg = ref()
</script>
<style lang="scss" scoped></style>


1.4.2 提供一个默认值(不推荐使用,会有问题)
defineModel 也能通过选项设置一个默认值,比如:
javascript
// 提供一个默认值
const model = defineModel({ default: 0 })
但是,这也可能会导致一个问题,就是父组件如果没有提供值(默认为undefined)的话,就会导致初始化的时候,父子组件数据不同步。
子组件 MyInput.vue:
javascript
<script setup>
// 提供一个默认值
const model = defineModel({ default: 0 })
</script>
<template>
<input v-model="model" />
</template>
父组件 App.vue:
javascript
<template>
<h2>{{ msg }}</h2>
<MyInput v-model="msg"/>
</template>
<script setup>
import MyInput from '@/components/MyInput.vue'
import { ref } from 'vue'
const msg = ref()
</script>
<style lang="scss" scoped></style>

1.4.3 指定类型
defineModel 也能通过选项设置值的类型,比如:
javascript
// 指定类型
const model = defineModel({ type: String })
子组件MyInput.vue:
javascript
<script setup>
// 指定类型
const model = defineModel({ type: String })
</script>
<template>
<input v-model="model" />
</template>
父组件 App.vue:
javascript
<template>
<h2>{{ msg }}</h2>
<MyInput v-model="msg"/>
</template>
<script setup>
import MyInput from '@/components/MyInput.vue'
import { ref } from 'vue'
const msg = ref(123)
</script>
<style lang="scss" scoped></style>


2. v-model 参数
2.1 给 v-model 命名
组件上的 v-model 也可以接受一个参数:
javascript
<MyComponent v-model:title="bookTitle" />
在子组件中,我们可以通过将字符串作为第一个参数传递给 defineModel() 来支持相应的参数:
javascript
<script setup>
const title = defineModel('title')
</script>
<template>
<input type="text" v-model="title" />
</template>


如果需要额外的 prop 选项,应该在 model 名称之后传递:
javascript
const title = defineModel('title', { required: true })
2.2. 多个 v-model 绑定
在 2.1 给 v-model 命名 中,我们知道可以给 v-model 绑定参数(相当于给双向绑定的prop进行命名)。这其实就意味着,我们可以在子组件中创建多个不同名称的 v-model,实现多个数据的双向绑定。
子组件 UserName.vue:
javascript
<template>
<div class="user-name">
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
</div>
</template>
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>
<style lang="scss" scoped>
.user-name{
margin-top: 10px;
}
input{
margin-right: 10px;
}
</style>
父组件 App.vue:
javascript
<template>
<div>{{first + ' ' + last}}</div>
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
</template>
<script setup>
import UserName from '@/components/UserName.vue'
import { ref } from 'vue'
const first = ref('Sheldon')
const last = ref('Zhou')
</script>
<style lang="scss" scoped>
</style>

3. v-model 修饰符
3.1 内置修饰符(表单修饰符)
v-model 内置的修饰符主要为 .trim,.number 和 .lazy。详情可参照 《Vue3 表单输入绑定》的 2.2 表单修饰符的使用 部分。下面是一些使用实例
javascript
<template>
<!-- lazy修饰符演示 -->
<input type="text" v-model.lazy="mess1" />
<p>你输入的是:{{ mess1 }}</p>
<p>类型为{{ typeof mess1 }}</p>
<p>长度为{{ mess1.length }}</p>
<!-- number修饰符演示 -->
<input type="text" v-model.number="mess2" />
<p>你输入的是:{{ mess2 }}</p>
<p>类型为{{ typeof mess2 }}</p>
<p>长度为{{ mess2.length }}</p>
<!-- trim修饰符演示 -->
<input type="text" v-model.trim="mess3" />
<p>你输入的是:{{ mess3 }}</p>
<p>类型为{{ typeof mess3 }}</p>
<p>长度为{{ mess3.length }}</p>
</template>
<script setup>
import { ref } from 'vue'
const mess1 = ref('')
const mess2 = ref('')
const mess3 = ref('')
</script>
<style scoped></style>
3.2 自定义修饰符
3.2.1 使用方式(从 defineModel() 解构,并且在 set 中自定义修饰符行为)
我们来创建一个自定义的修饰符 capitalize,它会自动将 v-model 绑定输入的字符串值第一个字母转为大写:
javascript
<MyComponent v-model.capitalize="myText" />
子组件 MyComponent.vue:
javascript
<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>
父组件 App.vue:
javascript
<template>
<MyComponent v-model.capitalize="myText" />
</template>
<script setup>
import MyComponent from '@/components/MyComponent.vue'
import { ref } from 'vue'
const myText = ref('')
</script>
<style lang="scss" scoped>
</style>

输入 hello 时,会将首字母自动转为大写 H。
3.2.2 不能用于初始值的转换(需要的话,建议改用计算属性)
如果我们在 3.2.1 中的 App.vue 的初始值直接赋值 hello:
javascript
const myText = ref('hello')

会发现,初始值并不会将首字母转为大写。因为这违背了 v-model 双向绑定的初衷(即双向绑定)。
因此,遇到这种需要对初始值进行修改的情况,我们最好在初始化的时候就对数据进行一次处理,或者直接弃用自定义修饰符,转而使用计算属性来实现。
3.3 带参数的修饰符(多个v-model 使用修饰符)
这里是另一个例子,展示了如何在使用多个不同参数的 v-model 时使用修饰符。
子组件 UserName.vue:
javascript
<template>
<div class="user-name">
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
</div>
</template>
<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName')
const [lastName, lastNameModifiers] = defineModel('lastName')
console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { uppercase: true }
</script>
<style lang="scss" scoped>
.user-name{
margin-top: 10px;
}
input{
margin-right: 10px;
}
</style>
父组件 App.vue:
javascript
<template>
<UserName
v-model:first-name.capitalize="first"
v-model:last-name.uppercase="last"
/>
</template>
<script setup>
import UserName from '@/components/UserName.vue'
import { ref } from 'vue'
const first = ref('Sheldon')
const last = ref('Zhou')
</script>
<style lang="scss" scoped>
</style>

上一章 《Vue3 自定义事件》