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