组件封装必学之实现v-model语法糖

2021-08-02  本文已影响0人  辉夜真是太可爱啦

本文将讲述如何在自定义的公用组件上实现 v-model,在实际项目的公共组件开发中有着很大的帮助!

学习目的

在自己封装组件的时候,特别是输入框,下拉选择框等交互组件的时候,一般绑定值的时候,采用的是 v-model,使用 v-model 的主要好处是无需记特定的 prop 字段名,即可绑定到组件中的值,降低组件的使用成本。

毕竟,一个好的公共组件,首先是 API 的设计应该让人容易理解,并且使用方便。

其次,应该尽量将重复的逻辑处理放在子组件中,这样子才会让组件的封装更有意义。

当然,通过本文的学习,即使不是交互组件,任何组件都可以通过这种方式来实现 v-model

下面就让我们一起来学习如何在公用组件上进行封装 v-model

v-model基本概念

v-model 实际上就是 $emit('input') 以及 props:value 的组合语法糖,只要组件中满足这两个条件,就可以在组件中使用 v-model

虽然在有些交互组件中有些许不同,例如:

checkboxradio 使用 props:checked 属性和 $emit('change') 事件。

select 使用 props:value 属性和 $emit('change') 事件。

但是,除了上面列举的这些,别的都是 $emit('input') 以及 props:value

实现数字计步器

既然已经知道了 v-model 的具体实现原理,那么,我们现在就来尝试自己封装一个数字计步器,主要由两个增减按钮,以及一个输入框组成。

新建组件

先新建一个 NumberInput 组件

<template>
  <div>
    <button>-1</button>
    <input type="text" />
    <button>+1</button>
  </div>
</template>

<script>
  export default {
    name: "NumberInput"
  }
</script>

props:value

先写上第一个条件, props:value ,由于是数字计步器,所以 value 的类型必定是数值型,并且,肯定是一个必定传递的参数。

<input type="number" :value="value" />

props:{
  value:{
    type: Number,
    default: 0,
    require: true
  }
}

当然,写到这里,就会有一个问题产生,由于 props 有一个特性,那就是单向数据流,对于但向数据流的理解。

你也可以称之为单向下行绑定,相当于就是父组件的值通过 props 传递给子组件,父组件中值的修改会传递到子组件,而子组件中对于这个值的修改则不能传递给父组件。(虽然可以,控制台会给出警告,而且官方也不推荐这样子做)

你可以想象成一个自上而下的瀑布,父组件在上,子组件在下,这个水流只能自上而下,而不能自下而上,那就不对劲了,牛顿大哥也不答应啊。

当然,这个单向数据流的出现,主要是为了防止从子组件意外更新父组件的状态,从而导致你的应用的数据流向难以理解。

$emit('input')

上面提及到的单向数据流中的有一个要点,那就是你不应该在一个子组件内部改变 props

所以,我们需要换一种方式来改写,通过定义一个变量 currentValue ,来避免对于子组件中 props 的直接修改,所有对于 <input> 输入框的修改,都通过这个 currentValue 来记录。

<input type="number" :value="currentValue" />

props:{
  value:{
    type: Number,
    default: 0
    require: true
  }
},
data(){
  return{
    currentValue: this.value
  }
}

而此时,我们先修改 currentValue 的值,然后通过 $emit('input') 来通知父组件,我们的 value 的值发生改变了,使父组件的 props 值进行修改,再通过父组件的单向数据流,让子组件中的值更新。从而避免对于子组件中 props 的直接修改。

<input type="number" :value="currentValue" @input="changeValue" />

methods:{
  changeValue(e){
    this.currentValue = parseInt(e.target.value);
    this.$emit('input', this.currentValue);
  }
}

现在,就完美地避开了单向数据流所带来的问题,通过一个 current 变量来记录值,并且避免对于 props 的直接修改。然后通过 $emit('input') 让父组件中绑定的值进行修改,通过 单向下行绑定 ,由父组件修改 props

那么,同理,左右两边的累加器也就很简单了,也是对 currentValue 值的修改,再加上 $emit('input') 的传递。

<button @click="increase(-1)">-1</button>
<button @click="increase(1)">+1</button>

methods:{
  increase(value){
    this.currentValue+= value;
    this.$emit('input', this.currentValue);
  }
}

至此,还有剩下一个很关键的步骤,那就是 watch 监听

watch 监听

当组件初始化时从 value 获取一次值,并且当父组件直接修改 v-model 绑定值的时候,对于 value 的及时监听就显得尤为重要。

所以,最后,我们还要加一步 watch 监听。

watch:{
  value(newVal){
    this.currentValue = newVal;
  }
},

至此,一个 v-model 的组件就封装好啦。

所以,完整的代码如下

<template>
  <div>
    <button @click="increase(-1)">-1</button>
    <input type="number" :value="currentValue" @input="changeValue" />
    <button @click="increase(1)">+1</button>
  </div>
</template>

<script>
  export default {
    name: "NumberInput",
    props:{
      value:{
        type: Number,
        default: 0,
        require: true
      }
    },
    data(){
      return{
        currentValue: this.value
      }
    },
    watch: {
      value(newVal){
        this.currentValue = newVal;
      }
    },
    methods:{
      changeValue(e){
        this.currentValue = parseInt(e.target.value);
        this.$emit('input', this.currentValue);
      },
      increase(value){
        this.currentValue+= value;
        this.$emit('input', this.currentValue);
      }
    }
  }
</script>

<style lang="stylus" scoped>

</style>

组件使用

<NumberInput v-model="number"></NumberInput>

import NumberInput from "./NumberInput";

export default {
  components:{NumberInput},
  data(){
    return{
      number: 10
    }
  }
}

文末总结

无论是任何组件,都可以实现 v-model

而实现 v-model 的要点,主要就是以下几点:

文末彩蛋 model

比方有些人说我就是不想用 props:value 以及 $emit('input') ,我想换一个名字,那么此时, model 可以帮你实现。

因为这两个名字在一些原生表单元素里,有其它用处。

export default {
  model: {
    prop: 'number',
    event: 'change'
  },
}

这种情况下,那就是使用 props:number 以及 $emit('change')

上一篇下一篇

猜你喜欢

热点阅读