Vue3 中 v-model 的原理是什么?和 Vue2 有什么区别?
简化版
v-model 本质是语法糖:父组件传值给子组件,子组件通过事件通知父组件更新。Vue3 默认使用 modelValue 和 update:modelValue,并支持多个 v-model;Vue2 默认是 value 和 input。
详细版
Vue3 组件上:
<UserInput v-model="name" />
等价于:
<UserInput
:modelValue="name"
@update:modelValue="name = $event"
/>
子组件需要声明:
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
Vue3 还支持多个 v-model:
<DateRange v-model:start="start" v-model:end="end" />
完整版教学
一、v-model 不是魔法,而是值和事件的约定
很多同学把 v-model 理解成“自动双向绑定”,但面试要说清楚它的展开形式。
父组件把状态传给子组件,子组件不能直接改父组件状态,只能发事件通知父组件更新。
所以它仍然符合单向数据流。
父组件 name
-> prop 传给子组件
-> 子组件输入变化 emit
-> 父组件接收事件更新 name
如果输入了 3 个字符,子组件通常会触发 3 次更新事件。 这解释了为什么复杂输入组件要考虑防抖、校验和更新时机。
二、原生表单上的 v-model 会映射到不同属性和事件
在原生表单元素上,v-model 会根据元素类型选择属性和事件。
文本框通常是 value + input,复选框可能是 checked + change。
这让模板更简洁。
<input v-model="name" />
可以理解成:
<input
:value="name"
@input="name = $event.target.value"
/>
| 元素 | 主要绑定 | 主要事件 |
|---|---|---|
| text input | value | input |
| checkbox | checked | change |
| select | value | change |
这个表不用死背,重点是知道 v-model 会根据控件类型做展开。
三、Vue3 组件 v-model 默认是 modelValue
Vue3 自定义组件的默认约定是 modelValue 和 update:modelValue。
父组件写起来像双向绑定,子组件内部仍然是 props + emit。
这比 Vue2 的 value/input 更统一。
<!-- 父组件 -->
<BaseInput v-model="keyword" />
<!-- 子组件 -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{
'update:modelValue': [value: string];
}>();
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</template>
这里子组件没有直接改 props.modelValue。
它只是发出新值,由父组件决定是否接收。
这也是调试自定义表单组件时最关键的一点。
四、Vue3 支持多个 v-model
Vue2 一个组件默认只有一个 v-model,多字段双向绑定常要借助 .sync。
Vue3 支持带参数的多个 v-model。
这对日期范围、坐标选择器、分页组件很常见。
<DateRange v-model:start="startDate" v-model:end="endDate" />
等价于:
<DateRange
:start="startDate"
:end="endDate"
@update:start="startDate = $event"
@update:end="endDate = $event"
/>
如果一个日期范围组件有 2 个值,Vue3 不需要把它们硬塞进一个对象。 这样字段更明确,父组件也更容易单独监听某个值变化。
五、修饰符会影响更新时机或值处理
v-model 常见修饰符有 .lazy、.number、.trim。
它们分别影响触发时机、数值转换、去除首尾空格。
自定义组件也可以接收 model modifiers 做定制处理。
<input v-model.trim="keyword" />
<input v-model.number="age" />
<input v-model.lazy="title" />
数字例子:用户输入 "18",普通 v-model 得到字符串 "18";使用 .number 后会尝试转成数字 18。
但转换失败时不应该假设一定是 number,仍要结合表单校验。
修饰符是输入处理辅助,不是完整校验方案。
六、不要在子组件里直接修改 prop
Vue 的单向数据流要求 prop 由父组件拥有。 子组件直接修改 prop 会破坏数据来源,也会触发警告。 正确做法是 emit 更新事件。
// 不推荐
props.modelValue = 'new';
// 推荐
emit('update:modelValue', 'new');
记忆钩子:Vue3 的组件 v-model 记成
modelValue进来,update:modelValue出去。
如果子组件需要本地临时编辑状态,可以复制一份内部状态,确认时再 emit。 这在弹窗编辑、复杂表单草稿里很常见。
七、常见误区与追问
- 误区:v-model 会让子组件直接修改父组件数据。 子组件只是发事件,真正更新发生在父组件表达式里。
- 误区:Vue3 的 v-model 仍默认是 value/input。 自定义组件默认变成
modelValue和update:modelValue。 - 误区:多个字段只能传一个对象做 v-model。 Vue3 支持多个带参数的
v-model,如v-model:start和v-model:end。 - 追问:Vue2 的 .sync 在 Vue3 里怎么替代? 通常用带参数的
v-model:propName和update:propName事件替代。 - 追问:自定义组件如何支持 v-model? 声明对应 prop,触发
update:xxx事件,父组件用v-model接收。 - 追问:什么时候不适合 v-model? 当子组件只是展示数据、更新逻辑复杂或需要显式命令语义时,用普通 prop 和事件更清楚。
八、加强记忆
这题用“语法糖展开”来回答。原生表单是属性加事件,自定义组件在 Vue3 默认是 modelValue 加 update:modelValue;多个字段用 v-model:xxx;最后强调不要直接改 prop,子组件通过 emit 通知父组件更新。