在 Vue3 组合式 API 中,组件间数据传递是核心需求之一。对于父子组件的双向绑定,Vue2 时代我们习惯用v-model 配合 value 属性和 input 事件,而 Vue3 最初引入了 setup 函数后,需要通过 props 接收值并手动触发事件来实现双向绑定。直到 Vue3.4 版本,官方正式推出了 defineModel 宏,彻底简化了父子组件双向绑定的实现逻辑。
本文将从 defineModel 的核心作用出发,逐步讲解其基础使用、进阶配置、常见场景及注意事项,帮助你快速掌握这一高效的 API。
一、为什么需要 defineModel?
在 defineModel 出现之前,实现父子组件双向绑定需要两步操作:
- 子组件通过
props接收父组件传递的值; - 子组件通过
emit触发事件,将修改后的值传递回父组件。
示例代码如下:
js
<!-- 子组件 Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const handleChange = (e) => {
emit('update:modelValue', e.target.value)
}
</script>
<template>
<input :value="props.modelValue" @input="handleChange" />
</template>
js
<!-- 父组件 Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const inputValue = ref('')
</script>
<template>
<Child v-model="inputValue" />
</template>
这种方式虽然可行,但存在明显弊端:代码冗余,每次实现双向绑定都需要重复定义 props 和 emit。而 defineModel 正是为了解决这个问题,它将 props 和 emit 的逻辑封装在一起,让双向绑定的实现更简洁、更直观。
二、defineModel 基础使用
2.1 基本语法
defineModel 是 Vue3.4+ 提供的内置宏,无需导入即可直接使用。其基本语法如下:
js
const model = defineModel();
通过上述代码,子组件即可直接获取到父组件通过 v-model 传递的值,且 model 是一个响应式对象,修改它会自动同步到父组件。
2.2 简化双向绑定示例
用 defineModel 重写上面的父子组件双向绑定示例:
js
<!-- 子组件 Child.vue -->
<script setup>
// 直接使用 defineModel 获取响应式模型
const modelValue = defineModel()
</script>
<template>
<!-- 直接绑定 modelValue,修改时自动同步到父组件 -->
<input v-model="modelValue" />
</template>
js
<!-- 父组件 Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const inputValue = ref('')
</script>
<template>
<Child v-model="inputValue" />
<p>父组件值:{{ inputValue }}</p>
</template>
可以看到,子组件的代码被大幅简化,无需再手动定义 props 和 emit,直接通过 defineModel 即可实现双向绑定。
2.3 自定义 v-model 名称
默认情况下,defineModel 对应父组件 v-model 的 modelValue 属性和 update:modelValue 事件。如果需要自定义 v-model 的名称(即多 v-model 场景),可以给 defineModel 传递一个参数作为名称:
js
<!-- 子组件 Child.vue -->
<script setup>
// 自定义 v-model 名称为 "username"
const username = defineModel('username')
// 再定义一个 v-model 名称为 "password"
const password = defineModel('password')
</script>
<template>
<div>
<input v-model="username" placeholder="请输入用户名" />
<input v-model="password" type="password" placeholder="请输入密码" />
</div>
</template>
js
<!-- 父组件 Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const user = ref('')
const pwd = ref('')
</script>
<template>
<Child
v-model:username="user"
v-model:password="pwd"
/>
<p>用户名:{{ user }}</p>
<p>密码:{{ pwd }}</p>
</template>
通过这种方式,我们可以轻松实现一个组件支持多个 v-model 绑定,满足复杂场景的需求。
三、defineModel 进阶配置
defineModel 还支持传入一个配置对象,用于设置默认值、类型校验、是否可写等属性,进一步增强组件的健壮性。
3.1 设置默认值
通过配置对象的 default 属性可以设置 v-model 的默认值:
js
<!-- 子组件 Child.vue -->
<script setup>
// 设置默认值为 "默认用户名"
const username = defineModel('username', {
default: '默认用户名'
})
</script>
<template>
<input v-model="username" placeholder="请输入用户名" />
</template>
此时,若父组件未给 v-model:username 传递值,子组件的 username 会默认使用 "默认用户名"。
3.2 类型校验
通过 type 属性可以对 v-model传递的值进行类型校验,支持单个类型或多个类型数组:
js
<!-- 子组件 Child.vue -->
<script setup>
// 限制 username 必须为字符串类型
const username = defineModel('username', {
type: String,
default: ''
})
// 限制 count 可以为 Number 或 String 类型
const count = defineModel('count', {
type: [Number, String],
default: 0
})
</script>
<template>
<input v-model="username" placeholder="请输入用户名" />
<button @click="count++">计数:{{ count }}</button>
</template>
若父组件传递的值类型不匹配,Vue 会在控制台给出警告,帮助我们提前发现问题。
3.3 控制是否可写
通过 settable 属性可以控制子组件是否能直接修改 defineModel 返回的响应式对象。默认情况下 settable: true,子组件可以直接修改;若设置为 false,子组件修改时会报错,只能通过父组件修改后同步过来。
js
<!-- 子组件 Child.vue -->
<script setup>
// 设置 settable: false,子组件不能直接修改
const username = defineModel('username', {
type: String,
default: '',
settable: false
})
const handleChange = (e) => {
// 报错:Cannot assign to 'username' because it's a read-only proxy
username.value = e.target.value
}
</script>
<template>
<input :value="username" @input="handleChange" />
</template>
这种配置适合需要严格控制数据流向的场景,确保数据只能由父组件修改。
3.4 转换值(getter/setter)
通过 get 和 set 方法可以对传递的值进行转换处理,类似计算属性的逻辑。例如,我们可以实现一个自动去除空格的输入框:
js
<!-- 子组件 Child.vue -->
<script setup>
const username = defineModel('username', {
get: (value) => {
// 父组件传递的值到子组件时,自动去除前后空格
return value?.trim() || ''
},
set: (value) => {
// 子组件修改后的值传递给父组件时,再次去除空格
return value.trim()
},
default: ''
})
</script>
<template>
<input v-model="username" placeholder="请输入用户名" />
</template>
通过 get 和 set,我们可以在数据传递的过程中对其进行加工,让组件的逻辑更灵活。
四、常见使用场景
4.1 表单组件封装
封装表单组件是 defineModel 最常用的场景之一。例如,封装一个自定义输入框组件,支持双向绑定、类型校验、默认值等功能:
js
<!-- 自定义输入框组件 CustomInput.vue -->
<script setup>
const props = defineProps({
label: {
type: String,
required: true
}
})
const modelValue = defineModel({
type: [String, Number],
default: '',
get: (val) => val || '',
set: (val) => val.toString().trim()
})
</script>
<template>
<div class="custom-input">
<label>{{ label }}:</label>
<input v-model="modelValue" />
</div>
</template>
js
<!-- 父组件使用 -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const name = ref('')
const age = ref(18)
</script>
<template>
<CustomInput label="姓名" v-model="name" />
<CustomInput label="年龄" v-model="age" />
<p>姓名:{{ name }},年龄:{{ age }}</p>
</template>
4.2 开关、滑块等UI组件
对于开关(Switch)、滑块(Slider)等需要双向绑定状态的UI组件,defineModel 也能极大简化代码。以开关组件为例:
js
<!-- 开关组件 Switch.vue -->
<script setup>
const modelValue = defineModel({
type: Boolean,
default: false
})
const toggle = () => {
modelValue.value = !modelValue.value
}
</script>
<template>
<div
class="switch"
:class="{ active: modelValue }"
@click="toggle"
>
<div class="switch-button"></div>
</div>
</template>
<style scoped>
.switch {
width: 60px;
height: 30px;
border-radius: 15px;
background-color: #ccc;
position: relative;
cursor: pointer;
}
.switch.active {
background-color: #42b983;
}
.switch-button {
width: 26px;
height: 26px;
border-radius: 50%;
background-color: #fff;
position: absolute;
top: 2px;
left: 2px;
transition: left 0.3s;
}
.switch.active .switch-button {
left: 32px;
}
</style>
js
<!-- 父组件使用 -->
<script setup>
import { ref } from 'vue'
import Switch from './Switch.vue'
const isOpen = ref(false)
</script>
<template>
<div>
<Switch v-model="isOpen" />
<p>开关状态:{{ isOpen ? '开启' : '关闭' }}</p>
</div>
</template>
五、注意事项
- Vue 版本要求 :
defineModel是 Vue3.4 及以上版本才支持的特性,若项目版本较低,需要先升级 Vue 版本(升级命令:npm update vue)。 - 响应式特性 :
defineModel返回的是一个响应式对象,修改其value属性会自动同步到父组件,无需手动触发emit事件。 - 与 defineProps 的关系 :
defineModel本质上是对props和emit的封装,因此不能与defineProps定义同名的属性,否则会出现冲突。 - 默认值的特殊性 :当
defineModel设置了default值时,若父组件传递了undefined,子组件会使用默认值;若父组件传递了null,则会使用null而不是默认值。 - 服务器端渲染(SSR)兼容性 :在 SSR 场景下,
defineModel完全兼容,无需额外处理,因为其底层还是基于props和emit实现的。
六、总结
defineModel 作为 Vue3.4+ 推出的重要特性,极大地简化了父子组件双向绑定的实现逻辑,减少了重复代码,提升了开发效率。它支持自定义名称、默认值、类型校验、值转换等多种进阶功能,能够满足大部分双向绑定场景的需求。
在实际开发中,对于需要双向绑定的组件(如表单组件、UI交互组件等),推荐优先使用 defineModel 替代传统的 props + emit 方式。同时,要注意其版本要求和使用规范,避免出现兼容性问题。