先记住这个答案
Vue 3 支持在同一组件上写多个 v-model。v-model:firstName 对应 firstName prop 与 update:firstName 事件,v-model:lastName 同理;子组件必须分别声明并在相应输入变化时发出完整新值。它取代了 Vue 2 中常见的 .sync 模式,并让字段名进入公开接口。多个通道彼此独立,如果业务要求名字两部分必须一起通过校验或原子提交,应额外提供 submit/change 事件或改用单个对象值,不能假设两个 update 事件会在同一批事务中被父级接受。
- 参数决定 prop 名称
- 事件按 update:参数名配对
- 独立字段不自动构成原子事务
参数直接定义组件公开字段
父级写 <UserName v-model:first-name="first" v-model:last-name="last" /> 时,编译器生成两组 prop 和事件监听。子组件无需接收一个含所有字段的大对象,也不会因为更新其中一个字段而原地修改另一个字段。
字段少、可独立编辑时,多 v-model 很直观;字段很多或存在跨字段约束时,大量 update 事件会扩大 API。此时可以使用表单对象加提交事件,让草稿、校验和持久化边界更清楚。
分别声明并发出两个更新事件
下例渲染两个输入,各自绑定对应 prop。父级对 firstName 的拒绝不会阻止 lastName 通道工作,因为组件只发意图,不在内部假定父级一定写回。
TypeScript 项目可用 defineEmits 泛型精确限定两个事件的载荷。测试除了检查事件名,还应改变父级两个状态并确认正确输入随之更新,避免 prop 与事件交叉连接。
import { defineComponent, h } from 'vue'
const UserName = defineComponent({
props: { firstName: String, lastName: String },
emits: ['update:firstName', 'update:lastName'],
setup(props, { emit }) {
return () => h('div', [
h('input', {
value: props.firstName,
onInput: (e: Event) => emit('update:firstName', (e.target as HTMLInputElement).value)
}),
h('input', {
value: props.lastName,
onInput: (e: Event) => emit('update:lastName', (e.target as HTMLInputElement).value)
})
])
}
})父级可分别绑定 v-model:first-name 和 v-model:last-name。每个输入只发送自己的候选值,子组件不复制或修改父级状态。
从 .sync 迁移时核对名称而非机械替换
Vue 2 的 :title.sync 常表达 update:title 约定,Vue 3 用 v-model:title 统一到 v-model 语法。迁移仍要检查组件是否曾发出其他名称、是否直接修改对象,以及默认 model 选项是否改变过 prop 和事件。
多个更新在同一用户动作中连续 emit 时,父级响应式渲染可能被批量处理,但业务监听器仍按调用顺序执行。若需要原子载荷,应发出一个包含完整快照的语义事件,避免依赖调度批处理冒充事务。
容易答错的地方
- prop 名与事件名没有严格配对
- firstName 必须配 update:firstName,大小写与拼写都属于契约。交叉发出事件会让父级另一个状态意外变化。
- 用多个 v-model 隐藏提交语义
- 输入中间态和最终提交是不同事件。需要一次性校验或保存时,应设计明确的 submit,并决定失败后如何保留草稿。
面试官还会怎么问?
每个 v-model 能有自己的修饰符吗?
可以,带参数通道会有相应的修饰符信息,例如 firstNameModifiers。组件要逐个声明支持范围,不能把一个字段的转换误用于另一个字段。
多个 update 事件会触发多次渲染吗?
同一同步调用栈中的响应式更新通常会批量刷新,但监听器本身按 emit 顺序运行。性能判断应以实际组件树和调度测量为准。
什么时候更适合单个对象 v-model?
字段经常一起替换、验证或撤销时,单对象快照更容易表达原子边界;仍应发送新对象,避免直接修改父级传入的引用。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。