Vue 组件 v-model
v-model如何实现父子组件数据同步?
一、先给结论
组件上的 v-model 不是子组件直接修改父组件状态,而是“prop + update 事件”的语法糖:
完整链路是:
本质仍然是单向数据流:数据向下,事件向上。v-model 只是把这套约定标准化并简化书写。
二、原生元素上的 v-model
原生元素和组件的展开规则不同,不能简单概括为所有场景都是 value + input。
所以面试时应区分:
- 原生元素:Vue 根据元素类型选择 DOM 属性和事件。
- 组件:Vue 根据组件约定生成 prop 和自定义事件。
三、Vue 3 手动实现组件 v-model
父组件
子组件
子组件不能直接修改 props.modelValue。它应该发出事件,让父组件修改自己的状态,再把新值传下来。
用计算属性封装读写
这里的计算属性没有创建一份无关的副本,而是把读取代理到 prop,把写入转换为 emit。
四、Vue 3.4+ 的 defineModel
Vue 3.4 开始,官方推荐使用 defineModel():
defineModel 是 <script setup> 的编译期宏,不是普通运行时函数。它会生成对应的 prop 和 update:modelValue 事件,概念上接近:
默认值的同步陷阱
如果子组件给 defineModel 设置默认值,但父组件没有传入对应 model,子组件和父组件可能暂时拥有不同的值:
此时子组件可能是 1,父组件对应状态仍是 undefined。因此要根据 API 设计决定 model 是否必填,不能把子组件默认值误认为父子已经完成同步。
五、多个 v-model
Vue 3 可以通过参数区分多个独立 model:
近似展开为:
命名规则是:
手动声明
使用 defineModel
六、Vue 2 的组件 v-model
Vue 2 默认协议是:
子组件:
model 选项
Vue 2 可以自定义默认 prop 和事件:
这样 <BaseCheckbox v-model="checked" /> 使用的就是 checked + change。
.sync 修饰符
.sync 是 Vue 2 针对任意 prop 的更新约定,不是完整的 v-model 替代品。Vue 3 使用 v-model:title 这类命名 model 表达相同场景。
七、v-model 修饰符
原生输入的内置修饰符
.trim:去除输入字符串首尾空白.number:尝试将输入转换为数字.lazy:通常从input时机改为change时机
修饰符改变的是读取或更新逻辑,不是给数据永久增加类型。
Vue 3.4+:通过 defineModel 处理自定义修饰符
父组件:
命名 model 的修饰符名称对应参数名:
手动读取修饰符
没有使用 defineModel 时,默认 model 可以声明 modelModifiers:
对于 v-model:title.capitalize,对应 prop 是 titleModifiers,而不是 modelModifiers。
八、编译与运行时边界
1. 不是双向引用
父组件传入的是当前值,子组件拿到的是 prop。真正修改父状态的是父组件生成的事件监听器:
2. 绑定目标必须可写
3. 对象 model 不是深度协议
如果 model 是对象,子组件拿到的可能是同一个对象引用。直接修改内部属性可能绕过 update 事件:
更清晰的写法是发出新对象:
具体是否复制对象取决于业务数据设计,但组件应明确谁拥有状态、谁负责更新。
4. 事件和 prop 名称必须匹配
默认协议必须同时使用 modelValue 与 update:modelValue;命名 model 必须同时使用 foo 与 update:foo。只改一侧,绑定就不会按预期工作。
九、设计原则
适合使用 v-model:
- 输入框、选择器、日期选择器等表单控件
- 复杂表单组件的当前值同步
- 一个组件有明确的“当前值 / 更新值”协议
- 一个组件有多个独立的可编辑值
不应滥用:
- 用它承载任意业务动作
- 子组件直接修改 prop
- 同时维护父状态和子状态两个真实数据源
- 用它替代语义明确的
submit、confirm、close等业务事件
例如弹窗更适合:
而不是把所有打开、关闭逻辑都隐藏在一个复杂的 v-model 中。
十、核心源码模型
下面是帮助理解的简化模型,不代表 Vue 源码:
组件上的 v-model 可以理解为:
真实 Vue 还会根据原生元素类型、参数、修饰符和版本生成不同的运行时指令或 vnode props;示例只是用于理解核心协议。
十一、Vue 2 与 Vue 3 对比
十二、面试官深挖
1. v-model 的本质是什么?
参考答案: 它是 prop 和更新事件的语法糖。Vue 3 默认是 modelValue + update:modelValue。父组件持有状态,子组件发事件请求更新,底层仍是数据向下、事件向上。
2. 原生元素和组件的展开规则一样吗?
参考答案: 不一样。原生元素根据类型处理 value、checked、selected 和不同事件;组件遵循 modelValue / update:modelValue 或命名 model 协议。
3. 子组件为什么不能直接修改 model prop?
参考答案: prop 属于父组件,直接修改会破坏单向数据流。子组件应 emit 更新事件,由父组件修改状态后重新下发。
4. Vue 2 和 Vue 3 有什么区别?
参考答案: Vue 2 默认是 value + input,可用 model 自定义;Vue 3 默认是 modelValue + update:modelValue,通过参数支持多个 model,3.4+ 推荐用 defineModel。
5. .sync 和命名 v-model 有什么关系?
参考答案: 两者都基于 prop 和 update 事件,但 .sync 是 Vue 2 针对任意 prop 的语法,Vue 3 用 v-model:title 统一表达多个双向值。
6. defineModel 是什么?
参考答案: 它是 <script setup> 编译期宏,会生成 prop 和 update: 事件,简化了手动声明,但没有改变 v-model 的通信本质。
7. 多个 v-model 解决什么问题?
参考答案: 一个组件可能有多个相互独立的可编辑值。命名 model 将它们映射到不同 prop 和事件,避免把所有值塞进一个对象。
8. 修饰符如何传给组件?
参考答案: 手动实现时,默认 model 使用 modelModifiers,命名 model 使用 nameModifiers;3.4+ 可以通过 defineModel 返回的第二个值获取修饰符,并在 setter 中转换值。
9. defineModel 的默认值有什么风险?
参考答案: 父组件未传入 model 时,子组件默认值和父状态可能暂时不同步。要明确 model 是否必填,不能把子组件默认值当成父子已经同步。
10. v-model 可以绑定任意表达式吗?
参考答案: 不能。编译器最终需要生成可写回的赋值目标,变量和可写属性通常可以,函数调用、常量和只读表达式不可以。
十三、常见错误回答
- “
v-model就是双向绑定”:应补充它实际是 prop + event,仍遵循单向数据流。 - “所有
v-model都是value + input”:这是 Vue 2 默认协议或文本输入的简化说法。 - “子组件可以直接改
modelValue”:应通过update:事件通知父组件。 - “
.sync就等于 Vue 3 的v-model”:思想相近,但语法和适用版本不同。 - “
defineModel会创建本地副本”:它默认封装的是 prop 读取和 update 事件,不等于无条件复制数据。
十四、可迁移的理解
核心关键词
语法糖、单向数据流、prop、update 事件、编译展开、命名 model
一句话本质
组件 v-model 是父组件状态通过 prop 向下传递、子组件通过 update: 事件向上请求更新的标准化协议。
1 分钟面试回答
Vue 组件上的
v-model本质是 prop 和事件的语法糖。Vue 3 默认展开为:modelValue和@update:modelValue,父组件持有状态,子组件通过 emit 请求更新,仍然是数据向下、事件向上。Vue 2 默认是value + input,Vue 3 支持多个命名v-model,3.4+ 可以用defineModel简化声明。子组件不能直接修改 prop,而应该发出更新事件。
3 分钟回答应补充
原生 input、checkbox、select 的展开差异;Vue 2 的 model / .sync;Vue 3 的手动实现、defineModel 和自定义修饰符。
10 分钟回答应补充
模板编译和运行时指令、model 参数命名规则、默认值同步风险、对象引用边界、可写表达式限制,以及为什么业务动作更适合使用语义明确的事件。

