Skip to content

组件 v-model

基本用法

v-model 可用于组件上,实现双向绑定。

从 Vue 3.4 开始,实现这一点的推荐方式是使用 defineModel() 宏:

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

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

<template>
  <div>父组件绑定的 v-model 是:{{ model }}</div>
  <button @click="update">递增</button>
</template>

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

Parent.vue
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>

在 playground 中试一试

内部原理

defineModel 是一个便捷宏。编译器会将其展开为以下内容:

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

这就是在 3.4 之前,上面展示的子组件应当如何实现:

Child.vue
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" 会被编译为:

Parent.vue
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:

Child.vue
vue
<script setup>
const model = defineModel({ default: 1 })
</script>
Parent.vue
vue
<script setup>
const myRef = ref()
</script>

<template>
  <Child v-model="myRef"></Child>
</template>

首先让我们回顾一下 v-model 在原生元素上的用法:

template
<input v-model="searchText" />

在底层,模板编译器会为我们将 v-model 展开成更冗长的等价写法。因此,上面的代码与下面的写法效果相同:

template
<input
  :value="searchText"
  @input="searchText = $event.target.value"
/>

当用于组件时,v-model 会展开成这样:

template
<CustomInput
  :model-value="searchText"
  @update:model-value="newValue => searchText = newValue"
/>

不过,要让它真正工作,<CustomInput> 组件必须做两件事:

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

下面是实际效果:

CustomInput.vue
vue
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

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

现在 v-model 应该可以在这个组件上正常工作:

template
<CustomInput v-model="searchText" />

在 Playground 中试一试

在该组件中实现 v-model 的另一种方式,是使用一个可写的 computed 属性,同时提供 getter 和 setter。get 方法应返回 modelValue 属性,set 方法则应触发对应的事件:

CustomInput.vue
vue
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
}
</script>

<template>
  <input v-model="value" />
</template>

v-model 参数

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

template
<MyComponent v-model:title="bookTitle" />

在子组件中,我们可以通过向 defineModel() 传入字符串作为其第一个参数来支持对应的参数:

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

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

在 Playground 中试一试

如果还需要 prop 选项,则应将它们放在模型名称之后传入:

js
const title = defineModel('title', { required: true })
3.4 之前的用法
MyComponent.vue
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>

在 Playground 中试一试

在这种情况下,子组件不再使用默认的 modelValue prop 和 update:modelValue 事件,而应当期望一个 title prop,并发出一个 update:title 事件来更新父组件的值:

MyComponent.vue
vue
<script>
export default {
  props: ['title'],
  emits: ['update:title']
}
</script>

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

在 Playground 中试一试

多个 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>

在 Playground 中试一试

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>

在 Playground 中试一试

vue
<script>
export default {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['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>

在 Playground 中试一试

处理 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() 传入 getset 选项。这两个选项会接收 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>

在 Playground 中试试

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>

在 Playground 中试试

添加到组件 v-model 上的修饰符会通过 modelModifiers prop 提供给组件。在下面的示例中,我们创建了一个组件,其中包含一个默认值为空对象的 modelModifiers prop:

vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  created() {
    console.log(this.modelModifiers) // { capitalize: true }
  }
}
</script>

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

注意,组件的 modelModifiers prop 包含 capitalize,其值为 true——这是因为它被设置在 v-model 绑定 v-model.capitalize="myText" 上。

既然我们已经设置好了 prop,就可以检查 modelModifiers 对象的键,并编写一个处理函数来更改发出的值。下面的代码中,我们会在 <input /> 元素触发 input 事件时将字符串首字母大写。

vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  methods: {
    emitValue(e) {
      let value = e.target.value
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      this.$emit('update:modelValue', value)
    }
  }
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

在 Playground 中试试

带参数的 v-model 修饰符

对于同时带有参数和修饰符的 v-model 绑定,生成的 prop 名称将是 arg + "Modifiers"。例如:

template
<MyComponent v-model:title.capitalize="myText">

相应的声明应为:

js
export default {
  props: ['title', 'titleModifiers'],
  emits: ['update:title'],
  created() {
    console.log(this.titleModifiers) // { capitalize: true }
  }
}

下面是另一个使用不同参数的多个 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>
vue
<script>
export default {
  props: {
    firstName: String,
    lastName: String,
    firstNameModifiers: {
      default: () => ({})
    },
    lastNameModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:firstName', 'update:lastName'],
  created() {
    console.log(this.firstNameModifiers) // { capitalize: true }
    console.log(this.lastNameModifiers) // { uppercase: true }
  }
}
</script>
组件 v-model has loaded