一、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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。