Vue3里v - model和defineModel咋区分?用法、原理、场景全解析
不少刚接触Vue3的同学,一看到v - model和defineModel就犯迷糊:这俩啥关系?在啥场景用?该咋选?今天咱用问答形式,把这些疑惑一次性讲明白。
问题1:Vue3的v - model基础用法是啥?和Vue2有啥不一样?
双向绑定的核心逻辑是父组件传值给子组件,子组件变化后通知父组件更新,v - model就是为了让双向绑定写起来更简便的语法糖。
在Vue2里,子组件用v - model得这么写:
<!-- 子组件(Vue2) -->
<template><input :value="value" @input="$emit('input', $event.target.value)" /></template>
<script>
export default { props: ['value'], emits: ['input'] }
</script>
父组件用<Child v - model="parentVal"/>,本质是value="parentVal"结合@input="parentVal = $event"。
到了Vue3,默认的props名和事件名变了:props默认叫modelValue,事件默认叫update:modelValue,所以Vue3里不用defineModel的传统写法得这样:
<!-- 子组件(Vue3传统写法) -->
<template><input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /></template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
父组件用法依旧是<Child v - model="parentVal"/>,只是底层的props和事件名有变化,这样改是为了让多参数v - model(比如v - model:foo)的语法更统一,后面讲多绑定场景时会更直观。
问题2:defineModel是干啥的?为啥说它简化了v - model开发?
Vue3.4之后新增的defineModel是编译时宏(可以理解成“语法层面的快捷方式”),专门解决子组件用v - model时“样板代码太多”的问题。
以前子组件要支持v - model,得手动做这些事:
- 用
defineProps声明接收modelValue; - 用
defineEmits声明触发update:modelValue; - 模板里还要把输入事件和
$emit绑定。
现在有了defineModel,一行代码就能搞定所有逻辑:
<!-- 子组件(用defineModel) -->
<template><input v - model="model" /></template>
<script setup>
import { defineModel } from 'vue'
const model = defineModel() // 这一行就等同于上面两行defineProps + defineEmits
</script>
你可以把model当作本地响应式变量来用,修改model.value时,会自动触发父组件的update:modelValue事件,不用手动写$emit,相当于Vue帮你把“声明props + 声明事件 + 触发事件”全自动化了,代码量直接减半!
问题3:defineModel实际项目咋用?能给个完整例子不?
咱以“父组件和子组件双向绑定用户名”为例,分别看父、子组件的代码和效果。
父组件(Parent.vue):
<template>
<div>父组件显示:{{ username }}</div>
<!-- 用v - model绑定子组件 -->
<MyInput v - model="username" />
<button @click="reset">重置用户名</button>
</template>
<script setup>
import { ref } from 'vue'
import MyInput from './MyInput.vue'
const username = ref('初始名')
function reset() {
username.value = '初始名' // 父组件改值,子组件会同步
}
</script>
子组件(MyInput.vue):
<template>
<!-- 直接用model双向绑定输入框 -->
<input type="text" v - model="model" placeholder="输入新用户名" />
<button @click="forceChange">强制改成“测试”</button>
</template>
<script setup>
import { defineModel } from 'vue'
const model = defineModel() // 自动关联父组件的v - model
function forceChange() {
model.value = '测试' // 子组件改值,父组件会同步
}
</script>
运行后能看到:
- 在输入框打字,父组件的
username会跟着变; - 点击父组件“重置”,子组件输入框会变回“初始名”;
- 点击“强制改成测试”,父组件
username会变成“测试”。
整个过程不用手动写props和emit,全靠defineModel自动处理双向绑定,是不是很省心?
问题4:v - model和defineModel的原理是啥?编译后发生了啥?
得从“语法糖”和“编译时处理”这两个角度来理解:
v - model的原理:
父组件写<Child v - model="x"/>,Vue编译后会变成:
<Child :modelValue="x" @update:modelValue="x = $event" />
所以v - model本质是“绑定props + 监听自定义事件”的语法糖,让双向绑定写起来更简洁。
defineModel的原理:
defineModel是编译时宏(只有在<script setup>里能用,因为编译阶段要做特殊处理),当你写const model = defineModel()时,Vue编译阶段会自动帮你:
- 生成
defineProps(['modelValue'])(声明接收父组件的modelValue); - 生成
defineEmits(['update:modelValue'])(声明触发更新事件); - 返回一个代理变量
model,修改model.value时,自动调用$emit('update:modelValue', 新值)。
所以你看到的“直接改model.value”,实际是Vue在编译后帮你做了$emit,底层还是基于“props向下传,事件向上抛”的单向数据流,没有破坏Vue的核心设计。
问题5:多个v - model绑定咋处理?defineModel支持多参数吗?
实际开发中,经常需要一个子组件双向绑定多个父组件变量(比如同时绑定“用户名”和“密码”),Vue3的v - model支持加参数,defineModel也能对应处理。
举个例子:父组件同时绑定username和password,子组件分别处理。
父组件(MultiBind.vue):
<template>
<div>
用户名:{{ username }} <br />
密码:{{ password }} <br />
</div>
<MyForm
v - model="username"
v - model:password="password"
/>
</template>
<script setup>
import { ref } from 'vue'
import MyForm from './MyForm.vue'
const username = ref('')
const password = ref('')
</script>
子组件(MyForm.vue):
<template>
<input placeholder="用户名" v - model="nameModel" />
<input type="password" placeholder="密码" v - model="pwdModel" />
<button @click="randomFill">随机填充</button>
</template>
<script setup>
import { defineModel } from 'vue'
// 处理默认v - model(对应username)
const nameModel = defineModel()
// 处理v - model:password(参数是password)
const pwdModel = defineModel('password')
function randomFill() {
nameModel.value = '随机名' + Math.random().toString().slice(2, 5)
pwdModel.value = 'pwd' + Math.random().toString().slice(2, 5)
}
</script>
这里的关键是:
- 父组件用
v - model:参数名绑定多个变量; - 子组件用
defineModel('参数名')对应接收,每个defineModel实例管理一个双向绑定; - 修改
nameModel.value或pwdModel.value时,各自触发对应的update:参数名事件,父组件对应变量更新。
要是用传统写法(手动写props和emit),得声明props: ['modelValue', 'password']和emits: ['update:modelValue', 'update:password'],再手动$emit,代码量会翻倍,用defineModel就简洁多了!
问题6:从Vue2升级到Vue3,v - model和defineModel要注意哪些点?
升级时主要解决“语法兼容”和“代码迁移”问题:
Vue2 → Vue3的v - model语法变化
Vue2子组件v - model依赖props: value + emit: input,Vue3默认改成props: modelValue + emit: update:modelValue,所以升级老组件时,得把:
props: { value: ... }改成props: { modelValue: ... };$emit('input', 新值)改成$emit('update:modelValue', 新值)。
用defineModel替代传统写法
如果项目用Vue3.4+,可以直接用defineModel替换手动声明的props和emit,减少代码,比如把上面升级后的传统写法,改成:
<template><input v - model="model" /></template> <script setup> const model = defineModel() // 自动处理modelValue和update:modelValue </script>
多参数绑定的兼容(Vue2的.sync修饰符)
Vue2里用.sync实现多参数双向绑定(比如<Child :foo.sync="x"/>),Vue3里统一成v - model:foo语法,defineModel也支持defineModel('foo'),所以升级时把.sync改成v - model:foo,子组件用defineModel('foo')就行。
code前端网