一、Vue2中v-model的本质,value input的语法糖
不少同学在学Vue的时候,会疑惑“Vue2里的modelValue是啥?和v-model有啥关系?” 其实这里容易把Vue2和Vue3的概念弄混,得先把版本差异理清楚,再一步步看双向绑定的逻辑,今天就从Vue2的v-model原理说起,聊聊modelValue的来龙去脉,还有实际开发里怎么用好双向绑定~
Vue2里,v-model是个典型的**语法糖**——它把“传值+监听事件”这两个步骤合并成一个指令,让代码更简洁,但原生组件和自定义组件的v-model,底层逻辑又有点区别,得拆开看:(1)原生表单元素的v-model
对于<input>、<textarea>、<select>这些原生表单组件,v-model的作用是“双向绑定数据”,比如写一个用户名输入框:
<template>
<div>
<input v-model="username" placeholder="请输入用户名">
<p>你输入的是:{{ username }}</p>
</div>
</template>
<script>
export default {
data() {
return {
username: ''
}
}
}
</script>
这段代码等价于手动绑定value属性和监听input事件:
<input :value="username" @input="username = $event.target.value" placeholder="请输入用户名" >
能看到,原生组件的v-model,核心是通过value属性把数据传给DOM,再通过input事件把用户输入的新值传回给Vue实例,这里的value是DOM元素的固有属性,input是DOM元素的固有事件,Vue只是做了语法层面的简化。
(2)自定义组件的v-model
如果是自己写的自定义组件(比如封装的搜索框、下拉选择器),v-model的逻辑和原生组件类似,但依赖的是Vue组件间的props和事件通信,举个例子:封装一个带搜索建议的输入组件<SearchInput>,让它支持v-model绑定父组件的searchText。
父组件用法:
<template>
<SearchInput v-model="searchText" />
<p>当前搜索内容:{{ searchText }}</p>
</template>
<script>
import SearchInput from './SearchInput.vue'
export default {
components: { SearchInput },
data() {
return {
searchText: ''
}
}
}
</script>
子组件SearchInput.vue实现:
<template>
<div class="search-input">
<input
:value="value"
@input="handleInput"
placeholder="搜索关键词"
>
<!-- 这里省略搜索建议列表的逻辑 -->
</div>
</template>
<script>
export default {
props: ['value'], // 接收父组件通过v-model传的值
methods: {
handleInput(e) {
// 把用户输入的新值,通过input事件抛给父组件
this.$emit('input', e.target.value)
}
}
}
</script>
这里的关键是:父组件用v-model时,会自动给子组件传一个叫value的props;子组件要更新值时,必须通过this.$emit('input', 新值)通知父组件,父组件收到input事件后,会自动更新自己的searchText变量——这就是自定义组件v-model的核心逻辑:props传value + 事件抛input。
“modelValue”从哪来?Vue3的新玩法
如果是Vue3项目,你会发现自定义组件的v-model逻辑变了:默认传的props叫modelValue,触发更新的事件叫update:modelValue,比如Vue3中写自定义组件:
<!-- 子组件MyInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- 父组件用法 -->
<MyInput v-model="searchText" />
这时候父组件的v-model,等价于:modelValue="searchText" @update:modelValue="searchText = $event",能看到,Vue3把原来Vue2中“value+input”的默认组合,改成了“modelValue+update:modelValue”,让v-model的逻辑更统一(不管原生还是自定义组件,底层思路一致),也更灵活(支持多个v-model绑定)。
那为啥Vue2里会提到modelValue?大概率是学习时把Vue3的概念代入Vue2了,Vue2本身没有“modelValue”这个默认的props名,它默认用的是value(针对表单类自定义组件),但Vue2也有办法实现“自定义v-model的props和事件名”,这就要用到model选项。
Vue2的model选项:自定义v-model的“绑定规则”
Vue2中,如果你不想让自定义组件的v-model依赖value和input,可以用model选项来指定v-model对应的props名称和触发更新的事件名称。
举个实际场景:做一个开关组件<Switch>,希望v-model绑定的是checked状态,触发更新的事件是change(因为开关更适合用change事件,而非input事件)。
子组件<Switch>实现:
<template>
<button
class="switch-btn"
:class="{ active: checked }"
@click="handleClick"
>
{{ checked ? '开' : '关' }}
</button>
</template>
<script>
export default {
props: {
checked: {
type: Boolean,
default: false
}
},
model: {
prop: 'checked', // 指定v-model对应的props名称
event: 'change' // 指定触发更新的事件名称
},
methods: {
handleClick() {
// 触发change事件,把新的状态抛给父组件
this.$emit('change', !this.checked)
}
}
}
</script>
<style scoped>
.switch-btn {
padding: 4px 12px;
border: 1px solid #eee;
border-radius: 4px;
}
.switch-btn.active {
background: #42b983;
color: #fff;
}
</style>
父组件用法:
<template>
<Switch v-model="isOpen" />
<p>开关状态:{{ isOpen ? '开启' : '关闭' }}</p>
</template>
<script>
import Switch from './Switch.vue'
export default {
components: { Switch },
data() {
return {
isOpen: false
}
}
}
</script>
这时,父组件的<Switch v-model="isOpen" />等价于:
<Switch :checked="isOpen" @change="isOpen = $event" />
能看到,Vue2通过model选项,实现了和Vue3中modelValue类似的效果——让v-model的绑定关系可配置,虽然名字不是modelValue,但思路是一致的:不再固定用value和input,而是允许开发者自定义v-model对应的props和事件。
Vue2的.sync修饰符:实现“多双向绑定”
Vue3里可以给一个组件绑多个v-model(比如<MyComp v-model:foo="a" v-model:bar="b" />),Vue2里要实现类似“多个数据双向绑定”的效果,得用.sync修饰符。
.sync也是个语法糖,它的逻辑是:父组件传一个props(比如title),子组件通过$emit('update:title', 新值)来通知父组件更新,举个实际例子:做一个弹窗组件<Modal>,需要同时双向绑定“显示状态(visible)”和“标题(title)”。
子组件<Modal>实现
<template>
<div class="modal-mask" v-show="visible">
<div class="modal-content">
<h2>{{ title }}</h2>
<button @click="handleClose">关闭弹窗</button>
<button @click="handleChangeTitle">修改标题</button>
</div>
</div>
</template>
<script>
export default {
props: {
visible: {
type: Boolean,
default: false
}, {
type: String,
default: '默认标题'
}
},
methods: {
handleClose() {
// 通知父组件更新visible为false
this.$emit('update:visible', false)
},
handleChangeTitle() {
// 通知父组件更新title为新值
this.$emit('update:title', '新的弹窗标题')
}
}
}
</script>
<style scoped>
.modal-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.3);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style>
父组件用法
<template>
<button @click="isShow = true">打开弹窗</button>
<Modal
:visible.sync="isShow" sync="modalTitle"
/>
<p>当前标题:{{ modalTitle }}</p>
</template>
<script>
import Modal from './Modal.vue'
export default {
components: { Modal },
data() {
return {
isShow: false,
modalTitle: '默认标题'
}
}
}
</script>
这里父组件的:visible.sync="isShow"等价于:
:visible="isShow"
@update:visible="isShow = $event"
``` sync="modalTitle"`等价于`:title="modalTitle" @update:title="modalTitle = $event"`。
可以发现,.sync的逻辑和Vue3中“v-model:xxx”的逻辑几乎一样,只是Vue2里叫`update:xxx`,Vue3里默认是`update:modelValue`,所以Vue2通过.sync,也能实现“一个组件绑定多个双向数据”的需求~
### 五、从Vue2到Vue3,v-model的设计思路有啥变化?
Vue2的v-model设计,其实有点“分裂”:
- 对原生组件,用`value`(DOM属性) + `input`(DOM事件);
- 对自定义组件,默认也用`value` + `input`,但可以通过`model`选项自定义;
- 想实现多双向绑定,得用`.sync`修饰符,而.sync的语法和v-model又不一样(一个是`:xxx.sync`,一个是`v-model`)。
Vue3则把这些逻辑**统一**了:
- 不管原生还是自定义组件,v-model默认基于`modelValue`(props) + `update:modelValue`(事件);
- 想自定义v-model的名称,直接用`v-model:xxx`,<MyComp v-model:foo="a" />`,对应props是`foo`,事件是`update:foo`;
- 这样一来,v-model和.sync的功能合并了,API更简洁,也避免了Vue2里“同一功能两种语法”的问题。
所以回到“Vue2里的modelValue”这个问题——它本身不是Vue2的概念,而是Vue3为了统一v-model逻辑提出的新默认props名,但学习时把Vue2的v-model、model选项、.sync和Vue3的modelValue联系起来,能更清晰看到Vue团队对“双向绑定”这个核心功能的设计演进:**从分散的语法糖,到统一、灵活的API**。
### 六、Vue2开发中,双向绑定的常见坑和解决思路
实际写Vue2代码时,关于v-model和双向绑定容易踩这些坑,得注意:
#### (1)自定义组件中,props接收的“value”不能直接修改
Vue的props是**单向数据流**——父组件传值给子组件,子组件只能读,不能直接改,所以在自定义组件里,拿到父组件传的`value`(或model选项指定的props)后,必须通过`$emit`事件通知父组件更新,不能自己赋值。
错误示例(子组件直接改props):
```vue
<template>
<input :value="value" @input="handleInput">
</template>
<script>
export default {
props: ['value'],
methods: {
handleInput(e) {
this.value = e.target.value // 错误!props是只读的,不能直接修改
}
}
}
</script>
正确示例(用$emit通知父组件):
handleInput(e) {
this.$emit('input', e.target.value) // 正确:触发事件,让父组件更新
}
(2)用model选项时,别忘在props里声明对应的属性
比如前面的<Switch>组件,model选项里写了prop: 'checked',那在props选项中必须声明checked,否则Vue会报错“Missing required prop: "checked"”。
反例(props没声明checked):
export default {
// props里没写checked,但是model里指定了prop: 'checked'
model: {
prop: 'checked',
event: 'change'
},
props: {
// 这里漏了checked的声明
},
// ...
}
正例(props声明checked):
props: {
checked: {
type: Boolean,
default: false
}
},
model: {
prop: 'checked',
event: 'change'
},
(3).sync修饰符和v-model一起用,别搞混事件名
有些同学会在同一个组件上同时用v-model和.sync,
<Child v-model="a" :b.sync="b" />
这本身没问题,但要注意:
- v-model对应的事件是
input(或model选项指定的事件); - .sync对应的事件是
update:b;
子组件里触发事件时,千万不能把事件名写混,比如子组件想更新b,必须用this.$emit('update:b', 新值),而不是this.$emit('input', 新值)。
(4)父组件给子组件传值,v-model和:value同时用会冲突
<Child v-model="a" :value="b" />
这时子组件的value props会同时接收a(来自v-model)和b(来自:value),导致值混乱,因为v-model已经帮你传了value(除非用model选项改了prop名),所以别重复传value。
Vue2的v-model和“modelValue”的联系
一句话概括:Vue2本身没有modelValue这个默认props,但Vue3用modelValue重构了v-model的逻辑。
Vue2里实现双向绑定,核心靠这几套组合拳:
- 原生组件v-model:
value(DOM属性) +input(DOM事件); - 自定义组件v-model:
value(props) +input(事件),或通过model选项自定义props和事件名; - 多双向绑定:.sync修饰符,基于
xxx(props) +update:xxx(事件)。
而Vue3的modelValue,是对Vue2这些逻辑的统一和简化——把分散的v-model、model选项、.sync整合成“v-model:xxx”语法,默认用modelValue作为props名,让双向绑定的API更一致、更灵活。
所以学Vue2时,重点掌握v-model的语法糖本质、自定义组件的v-model实现(value+input或model选项)、.sync的用法;等学Vue3时,再对比理解modelValue的设计,就能自然衔接,明白Vue团队对双向绑定的迭代思路啦~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。