Skip to main content

v-model 与受控组件

一、v-model

v-model 用于在表单输入元素或组件上创建双向绑定。

1、基本用法

v-model 的使用范围:

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

举个例子:

使用 v-model 前
<input
:value="text"
@input="event => text = event.target.value"
/>
使用 v-model 后
<input v-model="text" />

注意:v-model 会忽略任何表单元素上的 valuecheckedselected 属性。

2、v-model 的修饰符

  • .lazy: 监听 change 事件而不是 input
  • .number: 将输入的合法符串转为数字;
  • .trim: 移除输入内容两端空格。

2-1、.lazy

默认情况下,v-model 会在每次 input 事件后更新数据。添加 lazy 修饰符可以改为在每次 change 事件后更新数据:

举个例子:

<template>
<p>age: {{ age }}</p>
<input v-model.lazy="age" />
</template>

<script setup>
import { ref } from 'vue'
const age = ref('')
</script>

实现效果:

2-2、.number

number 修饰符可以让用户输入自动转为数字。

举个例子:

<template>
<p>age: {{ age }}</p>
<input v-model.number="age" />
</template>

<script setup>
import { ref } from 'vue'
const age = ref('')
</script>

实现效果:

2-3、.trim

trim 修饰符可以自动去除用户输入内容中两端的空格。

举个例子:

<template>
<p>age: {{ age }}</p>
<input v-model.trim="age" />
</template>

<script setup>
import { ref } from 'vue'
const age = ref('')
</script>

实现效果:

3、在组件上使用 v-model

<MyInput v-model="age" />

在自定义组件上使用 v-model,相当于:

<MyInput
:modelValue="age"
@update:modelValue="newVal => (age = newVal)"
/>

因此,要使组件能使用 v-model,组件内部需要做两件事:

  • 在内部原生元素的 value 上绑定 modelValue
  • 输入新值时在元素上触发 update:modelValue 事件并传递新值。

具体例子,封装组件:

MyInput.vue
<template>
<input
class="my-input"
:value="modelValue"
@input="handleInput"
/>
</template>

<script setup lang="ts">
interface InputProps {
/**
* 输入框值
*/
modelValue?: string | number
}

interface InputEmits {
/**
* 输入框输入时触发
*/
(type: 'update:modelValue', val: string | number): void
}

const {
modelValue = undefined,
} = defineProps<InputProps>()
const emit = defineEmits<InputEmits>()

const handleInput = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
emit('update:modelValue', newVal)
}
</script>

使用组件:

App.vue
<template>
<p>age: {{ age }}</p>
<MyInput v-model="age" />
</template>

<script setup>
import { ref } from 'vue'
import MyInput from './MyInput.vue'
// import MyInput from './MyInput.tsx'

const age = ref(123)
</script>

实现效果:

4、组件绑定多个 v-model

默认情况下,v-model 在组件上使用 modelValue 作为 prop,并以 update:modelValue 作为对应的事件。可以给 v-model 指定一个参数来更改这些名字,使用组件时通过 : + 参数 的方式控制对应的 v-model,例如:

MyInput.vue
<template>
<input
class="my-input"
:value="name"
@input="handleInput"
/>
</template>

<script setup lang="ts">
interface InputProps {
/**
* 输入框值
*/
name?: string | number
}

interface InputEmits {
/**
* 输入框输入时触发
*/
(type: 'update:name', val: string | number): void
}

const {
modelValue = undefined,
} = defineProps<InputProps>()
const emit = defineEmits<InputEmits>()

const handleInput = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
emit('update:name', newVal)
}
</script>

通过 v-model 参数的特性,可以给组件添加多个 v-model

MyInput.vue
<template>
<input
:value="name1"
@input="handleInput1"
/>
<input
:value="name2"
@input="handleInput2"
/>
</template>

<script setup lang="ts">
interface InputProps {
/**
* 输入框1值
*/
name1?: string | number
/**
* 输入框2值
*/
name2?: string | number
}

interface InputEmits {
/**
* 输入框1输入时触发
*/
(type: 'update:name1', val: string | number): void
/**
* 输入框2输入时触发
*/
(type: 'update:name2', val: string | number): void
}

const {
modelValue = undefined,
} = defineProps<InputProps>()
const emit = defineEmits<InputEmits>()

const handleInput1 = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
emit('update:name1', newVal)
}

const handleInput2 = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
emit('update:name2', newVal)
}
</script>

实现效果:

点击查看双向绑定原理

二、Vue 受控组件

1、一般受控

说明

一般情况下,组件通过传入 value 作为受控固定值,传入 defaultValue 作为非受控默认值;

其中 value 的值一旦固定,组件内部关联的变量将随之而固定,这就是所谓的受控;

defaultValue 即使固定不变,组件内部关联的变量也不受其影响,依然可以改变,这就是非受控。

index.vue
<template>
<button @click="handleSwitch">
{{ innerValue ? '开' : '关' }}
</button>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'

interface SwitchProps {
/**
* 开关固定值(受控)
*/
value?: boolean
/**
* 开关默认值(非受控)
* @default false
*/
defaultValue?: boolean
}

interface SwitchEmits {
/**
* 切换开关时触发
*/
(type: 'change', val: boolean): void
}

const {
value = undefined, // 注意这里传入的受控值需主动声明为 undefined
defaultValue = false
} = defineProps<SwitchProps>()

const emit = defineEmits<SwitchEmits>()

const _switchValue = ref(defaultValue)
const innerValue = computed(() => value ?? _switchValue.value)

const handleSwitch = () => {
const newVal = !innerValue.value
_switchValue.value = newVal
emit('change', newVal) // 这里需要主动调用外部传入的 change 函数
}
</script>

2、input 受控

说明

在 React 中,input / textarea 设置了 value 属性则为受控组件,可通过 onChange 事件配合 setState() 改变 value 值来更新输入框的值。

而在 Vue 中,input / textarea 设置 value 值,当 value 发生变化时,输入框中的值并不会跟着变化,而且即使 value 属性定死,输入框仍可以自由输入。另外,Vue 中 onInput 事件相当于 React 的 onChange 事件。

Vue 的 input 受控需要在 onInput 中通过 nextTick 单独处理。

index.vue
<template>
<input
ref="inputRef"
:value="innerValue"
@input="handleInput"
/>
</template>

<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'

interface InputProps {
/**
* 输入框固定值(受控)
*/
value?: string | number
/**
* 输入框默认值(非受控)
* @default false
*/
defaultValue?: string | number
}

interface InputEmits {
/**
* 输入框输入时触发
*/
(type: 'input', val: string | number, ev: Event): void
}

const {
value = undefined, // 注意这里传入的受控值需主动声明为 undefined
defaultValue = ''
} = defineProps<InputProps>()
const emit = defineEmits<InputEmits>()

const _inputValue = ref(defaultValue)
const innerValue = computed(() => value ?? _inputValue.value)

const inputRef = ref<HTMLInputElement>()

const handleInput = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
_inputValue.value = newVal
emit('input', newVal, e)
// 受控处理
nextTick(() => {
if (inputRef.value && innerValue.value !== inputRef.value.value) {
inputRef.value.value = innerValue.value as string
}
})
}
</script>

3、与 v-model 结合使用

在 Vue 中,还多了 v-model 语法糖来辅助组件受控,可通过以下方式进行受控组件封装:

index.vue
<template>
<input
ref="inputRef"
:value="innerValue"
@input="handleInput"
/>
</template>

<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'

interface InputProps {
/**
* 输入框固定值(受控)
*/
modelValue?: string | number
/**
* 输入框默认值(非受控)
* @default false
*/
defaultValue?: string | number
}

interface InputEmits {
/**
* 输入框输入时触发 v-model
*/
(type: 'update:modelValue', val: string | number): void
/**
* 输入框输入时触发
*/
(type: 'input', val: string | number, ev: Event): void
}

const {
modelValue = undefined, // 注意这里传入的受控值需主动声明为 undefined
defaultValue = ''
} = defineProps<InputProps>()
const emit = defineEmits<InputEmits>()

const _inputValue = ref(defaultValue)
const innerValue = computed(() => modelValue ?? _inputValue.value)

const inputRef = ref<HTMLInputElement>()

const handleInput = (e: Event) => {
const newVal = (e.target as HTMLInputElement).value
_inputValue.value = newVal
emit('input', newVal, e)
emit('update:modelValue', newVal)
// 受控处理
nextTick(() => {
if (inputRef.value && innerValue.value !== inputRef.value.value) {
inputRef.value.value = innerValue.value as string
}
})
}
</script>