Vue2里的v-model是啥?先搞懂双向数据绑定
p>在Vue2开发里,v-model绝对是高频出现的知识点,但不少同学刚接触时总会疑惑——它到底是怎么实现双向绑定的?和父子组件通信有啥关系?自定义组件时又该咋用?今天咱们就从基础到实战,把Vue2的v-model掰开揉碎讲清楚,不管是新手入门还是想深挖原理,看完这篇都能有收获~
p>v-model最直观的作用是「双向数据绑定」,在表单元素(像input、textarea、select这些)上,能让视图和数据实时同步,举个简单例子:<template>
<div>
<input v-model="message" placeholder="输入点内容"/>
<p>你输入的内容是:{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: ''
}
}
}
</script>
```时,input里的文字变了,下面的`{{ message }}`也跟着变;反过来,如果代码里主动改`message`的值(比如加个按钮触发`this.message = '强制修改'`),input里的内容也会更新,这就是「双向」——视图改数据跟着改,数据改视图也跟着改。</p>
p>但得明白:v-model本质是<strong>语法糖</strong>,它背后做了两件事:给表单元素绑定`value`属性,同时监听`input`事件,上面的代码等价于:</p>
```vue
<input :value="message" @input="message = $event.target.value" placeholder="输入点内容"/>
p>这里的input事件是原生DOM事件,当输入框内容变化时,浏览器触发input事件,Vue把事件参数($event)里的目标值(target.value)赋值给message,完成数据更新,这么一拆解,就能发现v-model不是「魔法」,而是「属性绑定 + 事件监听」的组合~
v-model的实现原理,为啥说它是语法糖?
p>刚才在表单元素上的例子,已经能看出v-model是语法糖,但得分原生表单元素和自定义组件两种情况细讲。原生表单元素的情况
p>Vue会根据表单元素类型,自动选择对应的「绑定属性」和「监听事件」:- 对于文本类输入框(text、textarea):v-model绑定`value`属性,监听`input`事件;
- 对于单选框(radio)、复选框(checkbox):v-model绑定`checked`属性,监听`change`事件(比如checkbox选定时触发`change`,把`checked`状态同步给数据)。
自定义组件的情况
p>如果在自定义组件上用v-model,原理是:父组件给子组件传一个`value`属性,子组件通过`$emit('input', 新值)`来通知父组件更新数据。 p>举个例子,封装一个子组件`MyInput`,让父组件用v-model和它做双向绑定:父组件用例:
<template>
<div>
<MyInput v-model="parentValue"/>
<p>父组件的值:{{ parentValue }}</p>
</div>
</template>
<script>
import MyInput from './MyInput.vue'
export default {
components: { MyInput },
data() {
return {
parentValue: ''
}
}
}
</script>
子组件MyInput.vue:
<template>
<input :value="value" @input="$emit('input', $event.target.value)"/>
</template>
<script>
export default {
props: ['value'] // 接收父组件传的value
}
</script>
p>子组件的input事件触发时,通过$emit('input', 新值),父组件的v-model就会把parentValue更新为这个新值,所以组件上的v-model,本质是「props接收value + 事件触发input」的组合~
<script>
export default {
model: {
prop: 'text',
event: 'update'
},
props: ['text']
}
</script>
p>此时父组件用v-model时,等价于:text="parentValue" @update="parentValue = $event",这种自定义方式在封装特殊组件(比如日期选择器、开关组件)时很有用,能让代码更语义化~
v-model和父子组件通信有啥关系?
p>父子组件通信的基础逻辑是「父传子用props,子传父用$emit」,而v-model其实是把这两个过程「封装」了,让双向绑定写起来更简洁。 p>比如不用v-model的话,父组件给子组件传值、子组件更新数据的代码会像这样: ```vue父组件的值:{{ parentValue }}
怎么给自定义组件写v-model?实战封装一个搜索组件
p>实际开发中,经常需要封装带双向绑定的组件(比如带清空按钮的搜索框、自定义下拉选择器),这里用「带清空功能的搜索组件」做例子,一步步讲怎么实现v-model。需求:
封装一个SearchInput组件,外部用v-model绑定搜索关键词,组件内输入时同步更新,点击「清空」按钮能把关键词置空。
步骤1:用默认v-model规则实现(prop是value,事件是input)
子组件SearchInput.vue:
<template>
<div class="search-input">
<input
type="text"
:value="value"
@input="$emit('input', $event.target.value)"
placeholder="请输入搜索关键词"
/>
<button @click="handleClear">清空</button>
</div>
</template>
<script>
export default {
props: ['value'], // 接收父组件的value
methods: {
handleClear() {
this.$emit('input', ''); // 触发input事件,传空值
}
}
}
</script>
<style scoped>
.search-input {
display: flex;
}
button {
margin-left: 8px;
}
</style>
父组件使用:
<template>
<div>
<SearchInput v-model="searchKey"/>
<p>当前搜索关键词:{{ searchKey }}</p>
</div>
</template>
<script>
import SearchInput from './SearchInput.vue'
export default {
components: { SearchInput },
data() {
return {
searchKey: ''
}
}
}
</script>
p>这样,输入框打字时,input事件触发,searchKey更新;点击「清空」按钮,触发input事件传空值,searchKey也会变成空——完美实现双向绑定~
步骤2:用model选项自定义prop和事件名(进阶)
p>如果想让prop叫「keyword」,事件叫「change」,可以用model选项:
子组件修改:
<script>
export default {
model: {
prop: 'keyword',
event: 'change'
},
props: ['keyword'], // prop名要和model里的prop一致
methods: {
handleClear() {
this.$emit('change', ''); // 触发change事件
}
}
}
</script>
父组件使用:
p>还是<SearchInput v-model="searchKey"/>,但此时等价于:keyword="searchKey" @change="searchKey = $event",这种方式适合组件内部逻辑和「value」「input」不搭的场景(比如封装开关组件,prop叫「checked」,事件叫「toggle」),能让代码更语义化~
v-model和.sync修饰符有啥不一样?
p>很多同学会把v-model和.sync搞混,其实它们都是语法糖,但适用场景不同。先看.sync修饰符:
它是给单个props属性做双向绑定的语法糖,比如父组件给子组件传title,子组件要更新title,用.sync的话:
<!-- 父组件 --> <Child :title.sync="docTitle"/>
等价于:
<Child :title="docTitle" @update:title="docTitle = $event"/>
p>子组件更新时要$emit('update:title', 新值)。
再看v-model:
它是针对「输入/交互」场景的双向绑定,一个组件一般只有一个v-model(Vue2中)。.sync可以给多个props分别加双向绑定,比如同时绑定title和content:
<Child :title.sync="docTitle" :content.sync="docContent"/>
p>总结区别:
- v-model:专注「输入类」场景,语法糖是` :value + @input`(或自定义事件);
- .sync:更灵活,用于普通props的双向更新,语法糖是` :prop + @update:prop`;
- 一个组件可以有多个.sync,但v-model通常只写一个(Vue2中)。
举个例子(封装弹窗组件):
需求:弹窗组件Modal需要控制「显示隐藏(visible)」和「标题(title)」的双向更新。
子组件Modal.vue:
<template>
<div v-if="visible" class="modal">
<h3>{{ title }}</h3>
<button @click="$emit('update:visible', false)">关闭</button>
<button @click="$emit('update:title', '新标题')">修改标题</button>
</div>
</template>
<script>
export default {
props: ['visible', 'title']
}
</script>
父组件使用:
<template>
<div>
<button @click="showModal = true">打开弹窗</button>
<Modal
:visible.sync="showModal"
:title.sync="modalTitle"
/>
<p>当前标题:{{ modalTitle }}</p>
</div>
</template>
<script>
import Modal from './Modal.vue'
export default {
components: { Modal },
data() {
return {
showModal: false,
modalTitle: '默认标题'
}
}
}
</script>
p>这里visible和title都用.sync,子组件通过$emit('update:visible', ...)和$emit('update:title', ...)更新父组件数据;而如果用v-model,一个组件只能处理一个属性的双向绑定,所以这种多属性更新的场景用.sync更合适~
实际开发中,v-model能解决哪些痛点?
p>v-model在项目里的应用场景特别多,分享几个常见的:表单处理:简化多输入项的双向绑定
p>比如登录页面有用户名、密码两个输入框,用v-model可以快速绑定数据:
<template>
<form @submit.prevent="handleLogin">
<input v-model="username" placeholder="用户名" />
<input type="password" v-model="password" placeholder="密码" />
<button type="submit">登录</button>
</form>
</template>
<script>
export default {
data() {
return {
username: '',
password: ''
}
},
methods: {
handleLogin() {
// 直接用this.username和this.password发请求
console.log('登录信息:', this.username, this.password)
}
}
}
</script>
p>如果不用v-model,每个输入框都要写:value和@input,代码会繁琐很多,v-model让表单和数据的绑定更简洁,减少重复代码~
自定义组件封装:让组件通信更高效
p>比如封装一个「带搜索建议的输入框」组件,用户输入时实时请求接口拿建议,选中建议后自动填充到输入框,用v-model可以让外部轻松拿到输入值:
子组件SearchWithSuggest.vue:
<template>
<div>
<input
:value="value"
@input="handleInput"
placeholder="输入关键词搜建议"
/>
<ul v-if="suggestList.length">
<li
v-for="(item, index) in suggestList"
:key="index"
@click="handleSelect(item)"
>
{{ item }}
</li>
</ul>
</div>
</template>
<script>
export default {
props: ['value'],
data() {
return {
suggestList: []
}
},
methods: {
handleInput(e) {
const val = e.target.value
this.$emit('input', val) // 同步输入值给父组件
// 模拟请求接口拿建议
setTimeout(() => {
this.suggestList = [val + '1', val + '2', val + '3']
}, 500)
},
handleSelect(item) {
this.$emit('input', item) // 选中建议后,把item传给父组件
this.suggestList = [] // 清空建议列表
}
}
}
</script>
父组件使用:
<template>
<div>
<SearchWithSuggest v-model="searchVal"/>
<p>最终搜索值:{{ searchVal }}</p>
</div>
</template>
<script>
import SearchWithSuggest from './SearchWithSuggest.vue'
export default {
components: { SearchWithSuggest },
data() {
return {
searchVal: ''
}
}
}
</script>
p>这样封装后,父组件不用关心子组件内部的搜索建议逻辑,只需要通过v-model拿到最终的输入值,大大提高了组件的复用性~
3
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


