在 Vue 3 中,defineModel 宏 是 Vue 3.3(2023 年 5 月发布)引入的一项新特性,旨在简化组件中 v-model 双向绑定的实现。它是 <script setup> 语法糖的一部分,提供了更直观、简洁的方式来定义组件的模型值(model value),减少样板代码,同时保持类型安全和响应式特性。
什么是 defineModel 宏?
- defineModel 是一个专为 <script setup> 设计的宏,用于声明组件的 v-model 绑定。
- 它简化了传统方式中需要手动定义 props(如 modelValue)和 emits(如 update:modelValue)的繁琐步骤。
- 作用:允许组件直接声明一个响应式的模型值,自动处理父子组件的双向数据同步。
背景
- 在 Vue 3 中,v-model 是一种常见的双向绑定机制,通常用于表单组件(如输入框、开关)或自定义组件。
- Vue 2 和 Vue 3.2 之前的实现需要显式声明 props 和 emits:
<script> export default { props: ['modelValue'], emits: ['update:modelValue'], setup(props, { emit }) { const updateValue = (value) => { emit('update:modelValue', value); }; return { updateValue }; }, }; </script>
- 这种方式代码冗长,尤其在 <script setup> 中,Vue 3.3 通过 defineModel 极大简化了流程。
核心特性
- 简洁性:
- 无需手动声明 props 和 emits,defineModel 自动生成 modelValue 属性和 update:modelValue 事件。
- 响应式:
- 返回一个 Ref 类型的响应式引用,类似于 ref,可直接在组件中读写。
- 类型安全:
- 与 TypeScript 集成良好,支持类型推导和校验。
- 灵活性:
- 支持自定义模型名称(例如 v-model:foo)和默认值/校验逻辑。
基本用法
1. 默认 v-model
在 <script setup> 中,使用 defineModel 声明默认的 modelValue:
<script setup>
import { defineModel } from 'vue';
// 声明 modelValue
const modelValue = defineModel();
</script>
<template>
<input v-model="modelValue" />
</template>
- 说明:
- modelValue 是一个响应式的 Ref 对象,可直接用于模板的 v-model。
- 父组件使用 v-model 绑定:
<template> <ChildComponent v-model="parentValue" /> </template> <script setup> import { ref } from 'vue'; import ChildComponent from './ChildComponent.vue'; const parentValue = ref('初始值'); </script>
- 子组件的 modelValue 与父组件的 parentValue 自动同步。
2. 自定义模型名称
支持自定义 v-model 的名称(例如 v-model:foo):
<script setup>
const foo = defineModel('foo');
</script>
<template>
<input v-model="foo" />
</template>
- 父组件使用:
<ChildComponent v-model:foo="parentFoo" />
- 说明:foo 对应子组件的 foo 属性和 update:foo 事件。
3. 设置默认值和校验
支持为模型值设置默认值或校验规则:
<script setup>
const modelValue = defineModel({
default: 0, // 默认值
set(value) {
return value >= 0 ? value : 0; // 校验:确保值非负
},
});
</script>
<template>
<input type="number" v-model="modelValue" />
</template>
- 说明:
- default:指定默认值,当父组件未提供值时使用。
- set:自定义 setter 函数,用于值校验或转换。
4. TypeScript 支持
defineModel 与 TypeScript 集成良好,支持类型声明:
<script setup lang="ts">
const modelValue = defineModel<string>({ default: '' });
</script>
<template>
<input v-model="modelValue" />
</template>
- 说明:通过泛型 <string> 指定 modelValue 的类型,确保类型安全。
高级用法
1. 多个 v-model
支持组件定义多个 v-model 绑定:
<script setup>
const name = defineModel('name', { default: '' });
const age = defineModel('age', { default: 18 });
</script>
<template>
<input v-model="name" placeholder="姓名" />
<input type="number" v-model="age" />
</template>
- 父组件:
<ChildComponent v-model:name="parentName" v-model:age="parentAge" />
2. 结合计算属性
通过计算属性对模型值进行转换:
<script setup>
import { computed } from 'vue';
const modelValue = defineModel<string>({ default: '' });
const upperCaseValue = computed({
get: () => modelValue.value.toUpperCase(),
set: (value) => (modelValue.value = value.toLowerCase()),
});
</script>
<template>
<input v-model="upperCaseValue" />
</template>
- 说明:父组件绑定小写值,子组件显示大写值。
3. 与其他 API 结合
结合 watch 或 watchEffect 监听模型值变化:
<script setup>
import { watch } from 'vue';
const modelValue = defineModel<string>({ default: '' });
watch(modelValue, (newValue) => {
console.log('modelValue 变为:', newValue);
});
</script>
<template>
<input v-model="modelValue" />
</template>
优点
- 减少样板代码:
- 无需手动定义 props 和 emits,代码更简洁。
- 对比传统方式:
// 传统方式
defineProps(['modelValue']);
defineEmits(['update:modelValue']);
// defineModel const modelValue = defineModel();
- 响应式集成:
- 自动返回 Ref 类型,支持响应式更新,无需额外 ref 包装。
- 类型安全:
- 与 TypeScript 深度集成,减少类型错误,适合复杂 H5 项目。
- 灵活性:
- 支持自定义模型名称、默认值和校验,适应多样化场景。
注意事项
- 仅限 <script setup>:
- defineModel 只能在 <script setup> 中使用,不支持选项式 API 或普通 setup 函数。
- 父组件必须使用 v-model:
- 如果父组件未绑定 v-model,defineModel 的默认值生效,但不会触发更新。
- 性能:
- defineModel 的响应式开销与 ref 相同,通常可忽略,但在高频更新场景中需注意。
- 跨域 <iframe>:
- 在 <iframe> 嵌套场景中,defineModel 仅控制父组件状态,无法直接操作 <iframe> 内部 DOM,需结合 postMessage 通信。
与传统方式的对比
特性 | 传统方式 (props + emits) | defineModel |
---|---|---|
代码量 | 较多(需定义 props 和 emits) | 简洁(单行声明) |
响应式 | 需手动结合 ref 或 computed | 自动返回 Ref 类型 |
类型推导 | 需显式声明类型 | 内置类型推导,TS 友好 |
自定义模型名称 | 支持,但需手动处理 | 内置支持(如 defineModel('foo')) |
默认值/校验 | 需在 props 中定义 | 内置支持 default 和 set |
实际案例:H5 表单组件
以下是一个完整的 H5 表单组件示例,使用 defineModel 实现用户名输入框,结合游客登录场景:
<!-- GuestInput.vue -->
<script setup lang="ts">
import { defineModel } from 'vue';
const username = defineModel<string>('username', {
default: '',
set(value) {
return value.trim(); // 去除首尾空格
},
});
</script>
<template>
<div class="input-box">
<input v-model="username" placeholder="请输入用户名" />
<button @click="username = `Guest_${Date.now()}`">生成游客名</button>
</div>
</template>
<style>
.input-box {
display: flex;
gap: 10px;
}
</style>
-
<!-- Parent.vue --> <script setup lang="ts"> import { ref } from 'vue'; import GuestInput from './GuestInput.vue'; const guestUsername = ref(''); </script> <template> <GuestInput v-model:username="guestUsername" /> <p v-if="guestUsername">当前用户名: {{ guestUsername }}</p> </template>
- 运行效果:
- 用户输入用户名或点击按钮生成游客名,父组件的 guestUsername 实时同步。
- 输入值自动去除首尾空格(通过 set 校验)。
总结
defineModel 是 Vue 3.3 引入的强大宏,极大简化了 <script setup> 中 v-model 的实现,减少了样板代码,提升了开发效率和类型安全。其在 H5 网站开发中的优势包括:
- 简洁性:单行声明双向绑定,适合快速开发表单或输入组件。
- 响应式:内置 Ref 类型,结合 watch、computed 等 API 实现动态逻辑。
- H5 场景:可用于游客登录表单、rParam 输入框、<iframe> 属性控制等。
- TypeScript:增强类型推导,适合复杂项目。