如何优雅的使用v-model

4,908 阅读5分钟

Vue框架的一大核心特性就是数据的双向绑定,通过数据的改变,反映到视图上不同的渲染,从而实现系统的响应式。

如果你对实现数据双向绑定的原理感兴趣,可以看我之前写的有关发布订阅模式的文章。

JS中观察者模式与发布订阅模式

而提到双向绑定,我们自然就要提到v-model指令。或许你知道并能熟练的在表单元素上使用v-model,但v-model能做的事不止如此,所以本文就和大家一起探索v-model的高级用法。

v-model是什么

v-model的本质是一个语法糖,看下面这个例子:

<input type="text" v-model="something"/>
// 等同于
<input type="text" v-bind:value="something" v-on:input="something=$event.target.value"/>
// 简写为
<input type="text" :value="something" @input="something=$event.target.value"/>

在官方文档中提到,v-model的使用有限制,只能用于

  • <input>
  • <select>
  • <textarea>
  • components

前三个的使用相信大家都不陌生,在知道了v-model的原理后,我们接下来试试在自定义组件中使用v-model。

自定义组件上的v-model

现在我们来封装一个input组件。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>

  </style>
</head>
<body>
  <div id="demo">
    <custom-input v-model="result"></custom-input>
    <span>输出:{{result}}</span>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.3.0/vue.js"></script>
  <script>
    Vue.component('CustomInput', {
      template: `
        <div>
          <span>输入:</span>
          <input
            type="text"
            :value="value"
            @input="$emit('input', $event.target.value)"
          />
        </div>
      `,
      props: ['value']
    })
    var demo = new Vue({
      el: '#demo',
      data: {
        result: '6666'
      }
    })
  </script>
</body>
</html>

结果:

最终我们实现了在子组件的input里输入内容,并将内容实时展示在父组件上的效果。

但是对于初学者来说,这个例子可能会产生一些混淆,因为input元素本身可以使用v-model属性,而且自带input事件,所以我们来稍微改写下这个例子:

<div id="demo">
  <custom-input v-model="result"></custom-input>
  <span>输出:{{result}}</span>
</div>
<script>
  Vue.component('CustomInput', {
    template: `
      <div>
        <span>输入:</span>
        <input
          type="text"
          v-model="innerValue"
          @input="onInput"
        />
      </div>
    `,
    props: ['value'],
    data () {
      return {
        innerValue: this.value
      }
    },
    methods: {
      onInput (e) {
        this.innerValue = e.target.value
        this.$emit('input', this.innerValue)
      }
    }
  })
  var demo = new Vue({
    el: '#demo',
    data: {
      result: '6666'
    }
  })
</script>

结果:

可以看到这个例子实现的效果和上一个是一样的,但是在这个例子里,我们为子组件增加了一个内部数据,用于绑定内部input数据的变化,并在子组件input事件监听到变化时,通过向上发送一个input事件将变化的数据传给了父组件,父组件上的v-model实际上相当于:

<custom-input :value="result" @input="result=arguments[0]"></custom-input>

所以由上我们可以知道,在父组件上使用v-model其实就是做了这两件事:

  • 向子组件传递一个名为value的参数
  • 监听子组件向上发送的一个名为input的事件

所以我们的子组件并不只限于使用类似input的表单元素,只要我们想,子组件可以是任何元素,例如我们可以这样:

<div id="demo">
  <custom-input v-model="result"></custom-input>
  <span>输出:{{result}}</span>
</div>
<script>
  Vue.component('CustomInput', {
    template: `
      <div>
        <span>内部数据:{{innerValue}}</span>
        <button @click="onClick">改变</button>
      </div>
    `,
    props: ['value'],
    data () {
      return {
        innerValue: this.value
      }
    },
    methods: {
      onClick (e) {
        this.innerValue = '数据变啦'
        this.$emit('input', this.innerValue)
      }
    }
  })
  var demo = new Vue({
    el: '#demo',
    data: {
      result: '原始数据'
    }
  })
</script>

结果:

到这里,想必你已经理解了自定义组件上使用v-model的原理和实现过程,有了基础,让我们更上一层楼,看看v-model更高级的用法。

v-model的高级用法

通过上面的解析,我们会发现,v-model总是会传递名为value的参数,监听名为input的事件,但是,当我们在创建一个自定义的单选框或复选框组件的时候,我们可能需要接收的是checked属性,传递onchagne事件。所以在Vue 2.2.0以上的版本,新增了自定义model。

官方手册描述:

类型:{ prop?: string, event?: string }

解释:允许一个自定义组件在使用 v-model 时定制 prop 和 event。

也就是说,我们可以在子组件里自定义v-model传入参数的名字和提交给父组件的事件类型,如此便捷,那事不宜迟我们马上来看看怎么使用。

封装一个checkbox组件

在不能使用自定义model以前,我们想要实现在checkbox上的v-model我们需要这样做:

<input type="checkbox" :checked="value" @change="value = $event.target.checked" />

在组件上:

<custom-checkbox v-model="result"></custom-checkbox>

Vue.component('custom-checkbox', {
  tempalte: `
    <input 
      type="checkbox"
      @change="$emit('input', $event.target.checked)"
      :checked="value"
    />
  `,
  props: ['value'],
})

当我们用上自定义的model后,情况就变成了这样:

<div id="demo">
  <custom-checkbox v-model="result"></custom-checkbox>
  <span>状态:{{result}}</span>
</div>
<script>
  Vue.component('CustomCheckbox', {
    template: `
      <div>
        单选框:
        <input
          type="checkbox"
          @change="$emit('change', $event.target.checked)"
          :checked="checked"/>
      </div>
    `,
    model: {
      prop: 'checked',
      event: 'change'
    },
    props: ['checked']
  })
  var demo = new Vue({
    el: '#demo',
    data: {
      result: true
    }
  })
</script>

结果:

显然,使用了自定义的model会更符合我们对checkbox的认知,使用起来也更加方便。

封装一个select组件

在业务场景中,我们常常会因为嫌弃select组件原生那简陋的样式而用ul自己实现一个下拉框组件,有了自定义model我们可以将组件写的更直观、便捷。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .select {
      margin-top: 10px;
      width: 150px;
      padding: 5px;
      border: 1px solid #999;
      border-radius: 9px;
    }
    .head {
      height: 30px;
      line-height: 30px;
    }
    ul {
      padding: 0;
      margin: 0;
    }
    li {
      list-style: none;
      height: 30px;
      line-height: 30px;
    }
    li + li {
      border-top: 1px solid #999;
    }
  </style>
</head>
<body>
  <div id="demo">
    <span>当前选中值:{{selected.value}} - {{selected.text}}</span>
    <custom-select v-model="selected" :options="items"></custom-select>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.3.0/vue.js"></script>
  <script>
    Vue.component('CustomSelect', {
      template: `
        <div class="select">
          <div @click="show=!show" class="head">{{selectedItem.text||'请选择'}}</div>
          <ul v-show="show">
            <li
              v-for="item in options"
              :key="item.value"
              @click="onSelect(item)"
            >
              {{item.text}}
            </li>
          </ul>
        </div>
      `,
      model: {
        prop: 'selected',
        event: 'change'
      },
      props: ['options', 'selected'],
      data () {
        return {
          selectedItem: this.selected,
          show: false
        }
      },
      methods: {
        onSelect (item) {
          this.selectedItem = item
          this.show = !this.show
          this.$emit('change', item)
        }
      }
    })
    var demo = new Vue({
      el: '#demo',
      data: {
        items: [
          { id: 1, value: 'a', text: '选项一' },
          { id: 2, value: 'b', text: '选项二' },
          { id: 3, value: 'c', text: '选项三' }
        ],
        selected: {}
      }
    })
  </script>
</body>
</html>

结果:

通过上面这个例子,我们就会发现使用v-model搭配自定义的model,可以让我们的组件业务逻辑变得更加清晰,也更利于后期使用。

v-model和修饰符

最后我们再来讲下v-model可用的修饰符,为我们的开发锦上添花。

.lazy

  • 作用:在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 (除了上述输入法组合文字时)。你可以添加 lazy 修饰符,从而转变为使用 change 事件进行同步
  • 使用:
<input v-model.lazy="msg" >
  • 效果:

.number

  • 作用:在默认情况下,v-model在每次input事件触发后将输入框的值与数据进行同步(除了上述输入法组合文字时)。添加lazy修饰符,从而转变为使用change事件进行同步
  • 使用:
<input v-model.number="age" type="number">

.trim

  • 作用:自动将用户的输入值转为数值类型
  • 使用:
<input v-model.trim="msg">

.sync

这个修饰符是在Vue 2.3.0以上版本中新增的,严格来说,这个并不是v-model的修饰符,而是v-bind的修饰符。在有些情况下,我们可能需要对一个prop进行“双向绑定”,不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。所以Vue官方推荐以 update:myPropName的模式触发事件取而代之。

// 子组件
this.$emit('update:title', newTitle)

// 父组件
<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
>
</text-document>

而.sync修饰符的作用就是简化这一过程

<text-document v-bind:title.sync="doc.title"></text-document>

所以我们会发现,.sync修饰符让一个v-bind绑定数据,看起来可以被子组件修改,而本质上还是一个父子组件传值的过程,这个过程,你也可以使用上一节我们说过的v-model结合自定义model的方法实现:

// 子组件
model: {
  prop: 'title',
  event: 'update'
}
this.$emit('update', newTitle)

// 父组件
<text-document v-model="doc.title"></text-document>

两种方式,在表达上有所差异,而其中的优劣就看实际的开发的需要了。

以上就是关于v-model使用的全部内容,如果有什么错误或者解释不到位的地方,欢迎大家评论交流。