Code前端首页关于Code前端联系我们

Vue2里的v-model是啥?先搞懂双向数据绑定

terry 3小时前 阅读数 6 #Vue

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>举个checkbox的例子感受下:

```vue ``` p>这里v-model等价于:

```vue ``` p>因为checkbox的选中状态由`checked`控制,状态变化时触发`change`事件,所以和text输入框的`input`事件有区别,Vue会根据表单元素类型「智能匹配」对应的属性和事件~

自定义组件的情况

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」的组合~

p>Vue2里还能通过model选项自定义组件v-model的「prop名」和「事件名」,比如子组件想把prop叫「text」,事件叫「update」,可以这样写:

<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 ``` p>对比用v-model的写法``,是不是简洁太多?所以v-model相当于把「父传子(props传value) + 子传父($emit触发input事件)」这两步合并成一个指令,让父子组件间的双向绑定更顺手~

p>但要注意:props是单向数据流,子组件不能直接修改父组件传的props值(比如子组件里写`this.value = '新值'`会报错),必须通过`$emit`通知父组件更新,v-model的本质还是遵循这个规则——子组件只是触发事件,让父组件自己改数据,所以不会破坏单向数据流的原则~

怎么给自定义组件写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分别加双向绑定,比如同时绑定titlecontent

<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>这里visibletitle都用.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前端网发表,如需转载,请注明页面地址。

发表评论:

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

热门