组件封装必学之实现v-model语法糖
本文将讲述如何在自定义的公用组件上实现
v-model
,在实际项目的公共组件开发中有着很大的帮助!
学习目的
在自己封装组件的时候,特别是输入框,下拉选择框等交互组件的时候,一般绑定值的时候,采用的是 v-model
,使用 v-model
的主要好处是无需记特定的 prop
字段名,即可绑定到组件中的值,降低组件的使用成本。
毕竟,一个好的公共组件,首先是 API
的设计应该让人容易理解,并且使用方便。
其次,应该尽量将重复的逻辑处理放在子组件中,这样子才会让组件的封装更有意义。
当然,通过本文的学习,即使不是交互组件,任何组件都可以通过这种方式来实现 v-model
。
下面就让我们一起来学习如何在公用组件上进行封装 v-model
。
v-model基本概念
v-model
实际上就是 $emit('input')
以及 props:value
的组合语法糖,只要组件中满足这两个条件,就可以在组件中使用 v-model
。
虽然在有些交互组件中有些许不同,例如:
checkbox
和 radio
使用 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
的要点,主要就是以下几点:
-
props:value
用来控制
v-model
所绑定的值。 -
currentValue
由于
单向数据流
的原因,需要使用currentValue
避免子组件对于props
的直接操作。 -
$emit('input')
用来控制
v-model
值的修改操作,所有对于props
值的修改,都要通知父组件。 -
watch
监听当组件初始化时从
value
获取一次值,并且当父组件直接修改v-model
绑定值的时候,对于value
的及时监听。
文末彩蛋 model
比方有些人说我就是不想用 props:value
以及 $emit('input')
,我想换一个名字,那么此时, model
可以帮你实现。
因为这两个名字在一些原生表单元素里,有其它用处。
export default {
model: {
prop: 'number',
event: 'change'
},
}
这种情况下,那就是使用 props:number
以及 $emit('change')
。