Skip to content

组件 v-model

v-model 是 Vue 中的双向数据绑定指令。它既可以配合表单元素一起使用,又能配合组件一起使用。示例代码如下:

html
<!-- 表单元素的双向数据绑定 -->
<input v-model="msg" />

<!-- 组件上的双向数据绑定 -->
<Counter v-model="count" />

defineModel() 宏函数

defineModel() 是 Vue3.4 新增的宏函数,极大的简化了实现组件上 v-model 的过程,它是实现组件上的 v-model 的核心 API。

基本用法

实现父子组件之间 count 值的双向数据绑定。

在父组件中进行 v-model 的绑定

  1. 在父组件中定义名为 num 的响应式数据,并实现数值的自增和自减功能;
  2. 在父组件中导入并使用 MyCounter 子组件,并通过 v-model="num" 把父组件的 num 值双向绑定给子组件;

父组件的示例代码如下:

vue
<script setup>
import MyCounter from './components2/MyCounter.vue'
import { ref } from 'vue'

const num = ref(0)
</script>

<template>
  <h1>父组件中的 num 值是:{{ num }}</h1>
  <button @click="num--">-</button>
  <button @click="num++">+</button>
  <hr />
  <MyCounter v-model="num" />
</template>

在子组件中调用 defineModel() 宏函数

在子组件中调用 defineModel() 函数,并使用 count 来接收其返回值。返回值 count 是可读可写的,子组件中对 count 值的修改会自动同步到父组件的 num 中:

vue
<script setup>
// 1. 接收父组件传入的 num 数值, 并转存到 count 中
//    请注意: 这里得到的 count 是"可读可写"的
const count = defineModel()
</script>

<template>
  <!-- 2. 在页面上读取 count 的值 -->
  <h3>count 的值是:{{ count }}</h3>
  <!-- 3. 修改 count 的值 -->
  <button @click="count--">-</button>
  <button @click="count++">+</button>
</template>

🚨 注意 🚨

组件的 Props 是只读的,而调用 defineModel() 得到的返回值是可读可写的。

defineModel() 的底层原理

defineModel() 简化了子组件中实现 v-model 功能的代码逻辑。在底层它用到了 PropsRefEmit 自定义事件。

image-20240805164817771

其中:

  • Props 负责接收父组件通过 v-model="num" 传入的数据,默认生成的 prop 名称是 modelValue
  • Ref 负责把只读的 modelValue prop 的值,转存到 count 响应式数据中;
  • Emit 负责把响应式数据的变更以自定义事件的形式传出给父组件,从而更新父组件中的数据,默认生成的事件名是 update:modelValue

手动实现原始的 v-model 功能

defineModel() 宏函数出现之前,程序员需要在子组件中使用 Props、Ref 和 Emit 这种最原始的方式实现组件上的 v-model 功能。

子组件 MyCounter2 的示例代码如下:

vue
<script setup>
const props = defineProps({
  num: Number
})
// 约定:用以实现组件上 v-model 功能的自定义事件,
// 其名称要以 update: 开头,并且在其后跟上要更新的 prop 的名称
defineEmits(['update:num'])

import { ref, onBeforeUpdate } from 'vue'
const count = ref(props.num)

// 如果传入的 num 值发生了变化,则更新本地 count 的值
onBeforeUpdate(() => {
  count.value = props.num
})
</script>

<template>
  <h3>count 的值是:{{ count }}</h3>
  <!-- 注意这里的 --count 和 ++count -->
  <!-- 是先对 --count 或 ++count 表达式求值,再把求值的结果作为参数传给 $emit -->
  <button @click="$emit('update:num', --count)">-</button>
  <button @click="$emit('update:num', ++count)">+</button>
</template>

父组件中的代码如下:

vue
<script setup>
import MyCounter2 from './components2/MyCounter2.vue'
import { ref } from 'vue'

const num = ref(0)
</script>

<template>
  <h1>父组件中的 num 值是:{{ num }}</h1>
  <button @click="num--">-</button>
  <button @click="num++">+</button>
  <hr />
  <!-- 通过 prop 向下传入数据 -->
  <!-- 通过监听自定义事件,把子组件传出的数据,转存到自身的响应式数据 num 中 -->
  <MyCounter2 :num @update:num="(value) => (num = value)" />
</template>

其它用法

v-model 的参数

设置 prop 名称

在默认情况下,子组件中调用 defineModel() 宏函数之后,会在子组件中生成一个名为 modelValue 的 prop。如果我们想要自定义这个 prop 的名称,可以在调用 defineModel() 的时候传入第 1 个参数:

js
const count = defineModel('numb')

现在,子组件中生成的 prop 名称就变成了 numb

image-20240805201124616

相应的,父组件需要通过 v-model:自定义的prop名称 的方式进行组件上的双向数据绑定:

html
<MyCounter v-model:numb="num" />

同时,子组件中生成的 emit 事件名称也变成了 update:自定义的prop名称 的格式:

image-20240805201646079

prop 校验

可通过 defineModel() 的第 2 个参数设置 prop 的校验规则:

js
const count = defineModel('numb', { required: true, type: Number })

🚨 注意 🚨

如果为 defineModel prop 设置了一个 default 值且父组件没有为该 prop 提供任何值,会导致父组件与子组件之间不同步。因此不推荐在这里使用 default 选项设置默认值。

设置 ref 名称

在子组件中调用 defineModel() 之后,它的返回值就是转存的响应式的 ref 数据。例如:

js
const count = defineModel('numb')

上面代码中的常量 count 就是 ref 的名称:

image-20240805202208627

想要修改 ref 的名称,只需要重新给这个常量命名即可~

多个 v-model 绑定

如果想让子组件支持绑定多个 v-model 数据,则调用 defineModel() 的时候必须要传入第 1 个参数,这是区分多个 v-model 的唯一标识。

子组件的代码如下:

js
<script setup>
const count = defineModel('numb', { required: true, type: Number, default: 2 })
const message = defineModel('msg')
</script>

<template>
  <h3>count 的值是:{{ count }}</h3>
  <button @click="count--">-</button>
  <button @click="count++">+</button>

  <hr />

  <h3>message 的值是:</h3>
  <!-- 由于 message 是局部的 ref, -->
  <!-- 因此可以和 input 元素进行双向数据绑定 -->
  <input type="text" v-model="message" />
</template>

父组件的代码如下:

vue
<script setup>
import MyCounter from './components2/MyCounter.vue'
import { ref } from 'vue'

const num = ref(0)
const info = ref('hello')
</script>

<template>
  <h1>父组件中的 num 值是:{{ num }}</h1>
  <p>info 的值是:{{ info }}</p>
  <button @click="num--">-</button>
  <button @click="num++">+</button>
  <hr />

  <!-- 这里通过 prop 的名称进行双向绑定的区分 -->
  <MyCounter v-model:numb="num" v-model:msg="info" />
</template>

天不生夫子,万古长如夜