跳到主要内容

Vue 2 / Vue 3 中 v-model 的区别

很多同学学 v-model 时,最容易记住一句话:它是“双向绑定”。这句话不算错,但如果你要真正写组件、做迁移、答面试,只记这个结论远远不够。

更准确地说,v-model 是一层语法糖:它帮你把“父组件传值给子组件”与“子组件通知父组件更新”这套样板代码收起来了。Vue 2 和 Vue 3 的核心思想没变,但组件协议、扩展能力、迁移方式发生了明显变化。

1. 先记结论:最大差异在“组件上的 v-model

先看最重要的结论表:

对比项Vue 2Vue 3
普通表单元素本质是 value/checked + 对应事件本质没变,仍然是表单值 + 对应事件
组件默认 propvaluemodelValue
组件默认事件inputupdate:modelValue
自定义默认协议model 选项不再用 model,改为 v-model:参数
一个组件可绑定几个 v-model通常 1 个支持多个
额外双向绑定写法常配合 .sync.sync 被移除,统一用 v-model:xxx
组件修饰符扩展能力弱,写法不统一更自然,可处理自定义修饰符
迁移重点value/inputmodel.sync改成 modelValue/update:modelValuev-model:xxx
怎么记最省事

v-model原生表单元素 上变化不大,在 自定义组件 上变化最大。面试和项目迁移里,真正高频考的几乎都是“组件上的 v-model”。

2. 先别急着背差异:先搞清楚 v-model 的本质

不管是 Vue 2 还是 Vue 3,v-model 的底层逻辑都可以概括成两步:

  1. 父组件把值传给表单元素或子组件
  2. 表单元素或子组件在值变化时通知父组件更新

比如在原生输入框上,这个语法糖可以先粗略理解为:

<input v-model="msg" />

约等于:

<input :value="msg" @input="msg = $event.target.value" />

所以你可以把 v-model 理解成:

v-model = :绑定值 + @更新事件

这个本质在 Vue 2 和 Vue 3 都没有变。变的是:组件默认使用什么 prop,发什么事件,以及框架允许你把这件事扩展到什么程度。

3. 原生表单元素:Vue 2 和 Vue 3 差异不算大

很多人一提“Vue 2 和 Vue 3 的 v-model 区别”,会误以为所有场景都大改了。其实不是。

在原生表单元素上,两代 Vue 的心智模型基本一致:

  • 文本输入框、textarea:通常围绕 value 和输入事件
  • checkboxradio:通常围绕 checked 和变更事件
  • select:通常围绕选中值和变更事件
  • .trim.number.lazy 这些常见修饰符,两代 Vue 都支持

也就是说:

  • 你写 <input v-model="keyword" /> 时,Vue 2 和 Vue 3 的使用体验差别不大
  • 真正会让你改代码、改组件设计的,是组件上的 v-model 协议
补一句

为了讲清主线,下面默认重点讨论“组件上的 v-model”。这也是面试官和迁移文档最关心的部分。

4. 组件上的 v-model:Vue 2 默认是 value + input

Vue 2 里,如果你在组件上这样写:

<BaseInput v-model="title" />

它默认等价于:

<BaseInput :value="title" @input="title = $event" />

这意味着 Vue 2 组件想接住 v-model,通常要做两件事:

  1. 接收一个名为 value 的 prop
  2. 在值变化时触发 input 事件

4.1 Vue 2 组件示例

父组件:

<template>
<BaseInput v-model="title" />
</template>

<script>
import BaseInput from './BaseInput.vue'

export default {
components: { BaseInput },
data() {
return {
title: 'Vue 2 标题',
}
},
}
</script>

子组件:

<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>

<script>
export default {
props: {
value: {
type: String,
default: '',
},
},
}
</script>

4.2 Vue 2 想改协议怎么办?

Vue 2 提供了 model 选项,允许你把默认的 value/input 改成别的名字:

<script>
export default {
model: {
prop: 'checked',
event: 'change',
},
props: {
checked: Boolean,
},
}
</script>

这样父组件的:

<MySwitch v-model="enabled" />

就会变成:

<MySwitch :checked="enabled" @change="enabled = $event" />

这能解决一部分问题,但它有两个明显限制:

  • 一个组件通常只有一组 v-model
  • 如果还想对别的 prop 做“双向同步”,常常要额外使用 .sync

5. 组件上的 v-model:Vue 3 默认是 modelValue + update:modelValue

Vue 3 里,同样的写法:

<BaseInput v-model="title" />

默认等价于:

<BaseInput
:modelValue="title"
@update:modelValue="title = $event"
/>

也就是说,Vue 3 的组件协议改成了:

  • prop:modelValue
  • event:update:modelValue

5.1 Vue 3 组件示例

父组件:

<template>
<BaseInput v-model="title" />
</template>

<script setup>
import { ref } from 'vue'
import BaseInput from './BaseInput.vue'

const title = ref('Vue 3 标题')
</script>

子组件:

<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>

<script setup>
defineProps({
modelValue: {
type: String,
default: '',
},
})

defineEmits(['update:modelValue'])
</script>

5.2 为什么 Vue 3 要这么改?

表面上只是名字变了,背后其实是在解决 Vue 2 的几个痛点:

  1. value 这个 prop 名过于常见,容易和组件自己的业务语义撞车
  2. input 这个事件名太泛,不够语义化
  3. Vue 2 对“多个双向绑定”的支持不自然
  4. .syncv-model 并存,学习和维护成本偏高

换成 modelValue / update:modelValue 后,规则更统一,也更容易扩展。

6. Vue 3 最关键的升级:支持多个 v-model

这是 Vue 3 相比 Vue 2 最实用的提升之一。

假设你有一个表单组件,同时维护标题和内容。Vue 2 通常只能给它设计一组默认 v-model,其他字段要么手写事件,要么配合 .sync

而 Vue 3 可以直接这样写:

<ArticleEditor
v-model:title="title"
v-model:content="content"
/>

这会分别展开为:

<ArticleEditor
:title="title"
@update:title="title = $event"
:content="content"
@update:content="content = $event"
/>

子组件只要对应声明即可:

<script setup>
defineProps({
title: String,
content: String,
})

defineEmits(['update:title', 'update:content'])
</script>

这件事很重要,因为它意味着:

  • 一个组件不再只有“唯一的那一个双向绑定值”
  • 组件 API 可以更贴近真实业务语义
  • 以前很多依赖 .sync 的设计,可以直接统一到 v-model:参数

7. .sync 的角色变了:Vue 2 常用,Vue 3 移除

Vue 2 里,如果你除了默认 v-model 之外,还想让某个 prop 支持“父子同步”,常见写法是:

<Dialog :visible.sync="visible" />

本质上它会变成:

<Dialog :visible="visible" @update:visible="visible = $event" />

到了 Vue 3,.sync 被移除了,因为它和新的 v-model:参数 在语义上已经高度重合。

也就是说,Vue 3 推荐直接写:

<Dialog v-model:visible="visible" />

所以从迁移角度看,可以直接记住:

  • Vue 2 的 .sync
  • Vue 3 统一改成 v-model:prop名
一个高频面试点

很多人会说“Vue 3 的 v-model 只是把 value 改成了 modelValue”。这只说对了一半。真正更大的升级,是它把 .sync 那一套也统一进来了,并原生支持多个 v-model

8. 自定义协议的方式也变了

8.1 Vue 2:依赖 model 选项

Vue 2 如果不想用默认的 value/input,通常写:

export default {
model: {
prop: 'checked',
event: 'change',
},
props: {
checked: Boolean,
},
}

8.2 Vue 3:依赖参数化 v-model

Vue 3 不再推荐搞一个全局默认协议改名,而是直接把“绑定哪个字段”写在模板里:

<MySwitch v-model:checked="enabled" />

它对应的组件协议就是:

  • prop:checked
  • event:update:checked

也就是:

<MySwitch :checked="enabled" @update:checked="enabled = $event" />

这种写法更直观,因为你一眼就能看出“到底是哪个字段在双向绑定”,不需要再进组件里读 model 配置。

9. 修饰符:Vue 3 在组件场景下更自然

原生输入框上的 .trim.number.lazy 两代 Vue 都常见,但 Vue 3 对组件 v-model 修饰符的支持更完整、更统一

例如:

<UserNameInput v-model.trim="username" />

在 Vue 3 的自定义组件里,组件可以拿到这些修饰符信息,再决定怎么处理。

如果你使用的是 Vue 3.4+ 的 <script setup>,还可以写得更简单:

<script setup>
const [modelValue, modifiers] = defineModel()

function handleInput(e) {
let value = e.target.value

if (modifiers.trim) {
value = value.trim()
}

modelValue.value = value
}
</script>

这里要注意两点:

  • defineModel() 只是更方便的语法糖
  • 它底层仍然遵循 modelValue / update:modelValue 这一套协议

10. 一段代码看懂迁移:Vue 2 改到 Vue 3 要改哪里

下面用一个最常见的输入组件举例。

10.1 Vue 2 写法

<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>

<script>
export default {
props: {
value: String,
},
}
</script>

父组件:

<SearchInput v-model="keyword" />

10.2 Vue 3 写法

<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>

<script setup>
defineProps({
modelValue: String,
})

defineEmits(['update:modelValue'])
</script>

父组件:

<SearchInput v-model="keyword" />

你会发现父组件几乎没变,真正改的是组件内部协议

10.3 如果原来用了 model 选项

Vue 2:

export default {
model: {
prop: 'checked',
event: 'change',
},
props: {
checked: Boolean,
},
}

Vue 3 更推荐:

<MySwitch v-model:checked="enabled" />

子组件:

<script setup>
defineProps({
checked: Boolean,
})

defineEmits(['update:checked'])
</script>

10.4 如果原来用了 .sync

Vue 2:

<Dialog :visible.sync="visible" />

Vue 3:

<Dialog v-model:visible="visible" />

11. 迁移清单:项目里重点搜这几类代码

如果你在做 Vue 2 -> Vue 3 迁移,和 v-model 相关的代码优先排查这些地方:

  1. 组件里是否还在接收 value prop
  2. 组件里是否还在 $emit('input', xxx)
  3. 组件里是否声明了 model 选项
  4. 父组件模板里是否还在使用 .sync
  5. 是否存在一个组件维护多个双向字段,但写法仍然分散混乱

最常见的替换关系可以直接记成:

Vue 2 写法Vue 3 写法
valuemodelValue
inputupdate:modelValue
model: { prop, event }v-model:prop + update:prop
:foo.sync="bar"v-model:foo="bar"

12. 常见误区

误区 1:v-model 就是“双向绑定”,所以数据流是双向的

不准确。

更准确的描述是:数据源仍然在父组件,子组件只是通过事件请求父组件更新。 所以本质上仍然符合“prop 下发、event 上抛”的单向数据流思路。

误区 2:Vue 3 的 v-model 只是改了名字

不准确。

除了名字变成 modelValue / update:modelValue,Vue 3 还带来了:

  • 多个 v-model
  • v-model:参数 统一替代很多 .sync 场景
  • 组件修饰符能力更自然

误区 3:父组件写法没变,所以迁移没成本

也不准确。

父组件很多时候看起来还是:

<MyInput v-model="msg" />

子组件实现几乎一定要改,尤其是老组件库、业务基础组件、表单组件封装层。

13. 总结

如果只用一句话总结:

Vue 2 和 Vue 3 的 v-model 本质都一样,都是“绑定值 + 更新事件”的语法糖;但 Vue 3 把组件协议升级成了 modelValue + update:modelValue,并通过 v-model:参数 解决了 Vue 2 里单一 v-modelmodel 选项、.sync 分散并存的问题。

你可以把两代 Vue 的区别浓缩为三句:

  • 原生表单元素:差异不大
  • 自定义组件:协议变化很大
  • Vue 3:更统一、更语义化、更适合复杂组件

14. 面试高频问答

Q1:Vue 2 和 Vue 3 的 v-model 最大区别是什么?

参考回答:

最大区别在组件协议。Vue 2 默认是 value + input,Vue 3 默认是 modelValue + update:modelValue。同时 Vue 3 支持多个 v-model,还能用 v-model:xxx 统一替代很多 Vue 2 里的 .sync 场景。


Q2:v-model 的本质是什么?

参考回答:

v-model 是语法糖,本质就是:

  • 把一个值传给表单元素或子组件
  • 在值变化时通过事件把新值抛回去

也就是“绑定值 + 更新事件”。


Q3:为什么 Vue 3 要把 value/input 改成 modelValue/update:modelValue

参考回答:

主要是为了统一和扩展:

  • valueinput 太通用,容易和业务语义冲突
  • Vue 2 一个组件通常只有一组 v-model
  • Vue 2 的 .syncv-model 分开记忆,不够统一

Vue 3 改成 modelValue/update:modelValue 后,再配合 v-model:xxx,就能自然支持多个双向绑定字段。


Q4:Vue 3 里多个 v-model 是怎么实现的?

参考回答:

v-model:title="title" 会展开成 :title="title" @update:title="title = $event"v-model:content="content" 同理。所以它本质上是把“默认的 modelValue 协议”参数化了。


Q5:Vue 2 的 .sync 和 Vue 3 的 v-model:visible 是什么关系?

参考回答:

它们解决的是非常接近的问题,都是“某个 prop 需要父子同步更新”。Vue 3 里 .sync 被移除,推荐统一写成 v-model:visible="visible",本质上更一致、更直观。


Q6:v-model 会破坏单向数据流吗?

参考回答:

不会。父组件仍然通过 prop 往下传值,子组件仍然通过事件通知父组件修改。v-model 只是把这套模式封装成了更短的写法,并没有让子组件直接篡改父组件状态。