v-model是什么?
v-model是vue给我们提供的一个内置指令(多用于表单组件的“双向数据绑定”),而内置指令的作用就是用来操作DOM的(比如v-show就是用来控制dom层元素的显示与否),它其实是封装了处理DOM的逻辑,从而简化了我们的代码。v-model的作用就是用来监听我们的视图层数据和逻辑层数据是否发生了改变,如果视图层数据改变了,相应的逻辑层数据也会随之改变,如果逻辑层改变了,相应的视图层也会跟着改变,这种v-model内置指令的功能完全可以通过vue的自定义指令或其它方式来手动实现,只不过作者已经为我们封装好了,用于模拟双向数据绑定。为什么说是模拟呢?vue难道不就是双向数据绑定的吗?请继续往下看。
v-model指令双向数据流的实现
什么是双向数据绑定?简单来说数据既可以流入,也可以流出。视图层的改变会影响数据层的改变,数据层的改变也会相应的改变视图层。v-model指令所实现的效果让很多人误认为vue实现了双向数据流,其实不然,vue只是一个单向的数据流,我们可以翻源码来看看v-model是如何实现双向数据流的效果的,大体上实现原理如下:
<input type='text' v-model='msg'>
// 相当于
<input type='text' :value=msg @input='msg =$event.target.value'>
可以看出v-model本质是一个语法糖,在v-bind和v-on的作用中,表单元素比如input,v-bind绑定一个值,就把data数据传给了value,同时再通过v-on监听input事件,当表单数据改变的时候,也会把值传给data数据,这样就模拟了双向数据绑定。
v-model指令的进阶应用
PS:项目中需要做一个自定义的复选框组件,恰好需要在组件上用到v-model指令,做完的效果如下图:
绝大多数情况下,我们会将v-model指令直接作用于表单元素,用于模拟双向数据流。但当我们需要将其作用于自定义组件上时,应该如何处理呢?其实vue2.2新增model API已经为我们实现了。
允许一个自定义组件在使用 v-model 时定制 prop 和 event。默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event,但是一些输入类型比如单选框和复选框按钮可能想使用 value prop 来达到不同的目的。使用 model 选项可以回避这些情况产生的冲突。
以上是API的原话,可能刚一看不是很好理解,那我们就进行分析。
首先我们来分析“允许一个自定义组件在使用 v-model 时定制 prop 和 event。默认情况下,一个组件上的v-model会把value用作prop且把input用作event”。一般说来,v-model用在表单元素上进行数据的双向绑定,自定义组件通常通过父子组件传值绑定数据。前边说了,v-model是v-bind和v-on的语法糖,那么在组件中使用v-model就完成如下两个步骤:
// 父组件
<Child v-model='iptValue'></Child>
// 子组件
Vue.components('Child',{
model: {
prop: ipt,
evnet: change
}
props: {
ipt: Number
}
template: `<input type='number' :value='ipt' @change='$emit("change",parseInt($event.target.value))'>`
})
这里父组件中的v-model相当于:
<Child :value='iptValue' @change='value => iptValue = value'></Child>
前面说了,父子组件传值通过prop和emit,第一步父组件把iptValue通过prop传给了子组件,但要注意,我这里的子组件给prop取了一个别名叫做ipt作为区分,所以子组件的prop对象中的键为我取的别名ipt。第二步,当子组件input值改变的时候,子组件监听了一个onchange方法,注意我这里也给emit中的事件取了一个别名,只不过这个别名和原来的名字一样change,input值改变emit提交change事件并把新值传给父组件,要注意,emit的荷载都是字符串。
父组件解析的代码可以理解为父组件执行value => iptValue = value语句,这样就完成了父子组件数据的双向绑定。个人觉得v-model用在自定义组件最大的好处是提高了组件的封装性,父组件不必要另外写一个接受子组件发送给来的emit方法。
v-model指令在组件中使用示例
前面复选框组件截图代码如下:
第一部分(用于演示复选框组件的容器)
<template>
<div class="show-box">
<div class="change-box">
<div class="name">主题切换:</div>
<label>
<input name="theme" class="theme" type="radio" value="1" v-model="picked"/>
<div class="show-color theme-1">主题一</div>
</label>
<label>
<input name="theme" class="theme" type="radio" value="2" v-model="picked"/>
<div class="show-color theme-2">主题二</div>
</label>
<label>
<input name="theme" class="theme" type="radio" value="3" v-model="picked"/>
<div class="show-color theme-3">主题三</div>
</label>
</div>
<div class="title">样式复选框(一)</div>
<checkbox :theme="picked" v-model="item.checked" v-for="(item,index) of test" :key="item.value" :name="item.name" :disabled="item.disabled"></checkbox>
<div class="title">样式复选框(二)</div>
<checkbox-slide class="wrap" :theme="picked" v-model="item.checked" v-for="(item,index) of test2" :key="item.value" :name="item.name" :disabled="item.disabled"></checkbox-slide>
</div>
</template>
<script>
import Checkbox from "./Checkbox.vue"
import CheckboxSlide from "./CheckboxSlide.vue"
export default {
components:{
Checkbox,
CheckboxSlide
},
data(){
return {
picked:"1",
test:[
{"value":1,"checked":true,"name":"选项一","disabled":false},
{"value":2,"checked":false,"name":"选项二","disabled":false},
{"value":3,"checked":true,"name":"选项三","disabled":true},
{"value":4,"checked":false,"name":"选项四","disabled":true}
],
test2:[
{"value":1,"checked":true,"name":"选项一","disabled":false},
{"value":2,"checked":false,"name":"选项二","disabled":false},
{"value":3,"checked":true,"name":"选项三","disabled":true},
{"value":4,"checked":false,"name":"选项四","disabled":true}
]
}
},
watch:{
picked(val){
this.picked=val;
}
},
methods:{
}
}
</script>
<style scoped lang="scss">
.show-box{
padding: 0px 60px 30px 60px;
.change-box{
margin-top: 10px;
margin-bottom: 20px;
overflow:hidden;
height:25px;
.name{
float: left;
font-size: 14px;
color: #666;
line-height: 25px;
margin-right: 16px;
}
label{
display: block;
height: 25px;
float: left;
margin-right: 16px;
.theme{
float: left;
margin-top: 6px;
margin-right: 4px;
}
.show-color{
font-size: 12px;
line-height: 25px;
text-align: center;
float: left;
width: 80px;
height: 25px;
line-height: 25px;
color: #fff;
user-select: none;
}
.theme-1{
background-color: #0099FF;
}
.theme-2{
background-color: #C23031;
}
.theme-3{
background-color: #FFAF7A;
}
}
}
.title{
font-size: 18px;
font-weight: bold;
color: #666;
line-height: 40px;
}
.wrap{
margin-bottom: 10px;
}
}
</style>
第二部分(样式复选框一)
<template>
<div class="check-box" :class="{'theme-1':theme=='1','theme-2':theme=='2','theme-3':theme=='3'}">
<label class="label" :class="{'checked':checked,'disabled':!!disabled}">
<div class="frame-box">
<span class="iconfont icon-right" v-show="checked"></span>
</div>
<div class="checkbox-text">{{name}}</div>
<input class="input" :disabled="disabled" type="checkbox" name="" id="" :checked="checked" @change='$emit("change",$event.target.checked)'/>
</label>
</div>
</div>
</template>
<script>
export default {
model: {
prop: 'checked',
event: 'change'
},
props:{
checked:{
type:Boolean,
required: true,
},
name:{
type:String,
required:true,
},
disabled:{
type:Boolean,
default:false,
},
theme:{
type:String,
required: true,
}
},
data(){
return {
a:false
}
},
methods:{
}
}
</script>
<style lang="scss" scoped>
.check-box{
height: 18px;
display: inline-block;
user-select: none;
.label{
cursor: pointer;
display: inline-block;
height: 18px;
.input{
position: absolute;
z-index: -1;
}
.frame-box{
float: left;
height: 18px;
width: 18px;
box-sizing: border-box;
border: solid 1px #ccc;
background-color: #fff;
border-radius: 3px;
text-align: center;
.iconfont{
font-size: 12px;
color: #fff;
line-height: 16px;
}
}
.checkbox-text{
padding: 0 6px;
line-height: 18px;
float: left;
font-size: 14px;
color: #999;
}
}
.checked{
.frame-box{
border-color: #0099FF;
.iconfont{
color: #0099FF;
}
}
.checkbox-text{
color: #0099FF;
}
}
.disabled{
cursor: not-allowed;
.frame-box{
border-color: #D7D7D7 !important;
background-color: #D7D7D7 !important;
.iconfont{
color: #fff !important;
}
}
.checkbox-text{
color: #D7D7D7 !important;
}
}
}
.theme-1{
.checked{
.frame-box{
border-color: #0099FF;
.iconfont{
color: #0099FF;
}
}
.checkbox-text{
color: #0099FF;
}
}
}
.theme-2{
.checked{
.frame-box{
border-color: #C23031;
.iconfont{
color: #C23031;
}
}
.checkbox-text{
color: #C23031;
}
}
}
.theme-3{
.checked{
.frame-box{
border-color: #FFAF7A;
.iconfont{
color: #FFAF7A;
}
}
.checkbox-text{
color: #FFAF7A;
}
}
}
</style>
第三部分(样式复选框二)
<template>
<div class="check-box" :class="{'theme-1':theme=='1','theme-2':theme=='2','theme-3':theme=='3'}">
<label class="label" :class="{'checked':checked,'disabled':!!disabled}">
<div class="frame-box">
<span class="iconfont icon-right" v-show="checked"></span>
</div>
<div class="bg-box" :class="{'bg-box-active':checked}">
<div class="uncheck"></div>
<div class="checked"></div>
</div>
<div class="text-box">
<span class="checked-text" v-if="checked">选中</span>
<span class="uncheck-text" v-else>未选中</span>
</div>
<input class="input" :disabled="disabled" type="checkbox" name="" id="" :checked="checked" @change='$emit("change",$event.target.checked)'/>
</label>
</div>
</div>
</template>
<script>
export default {
model: {
prop: 'checked',
event: 'change'
},
props:{
checked:{
type:Boolean,
required: true,
},
name:{
type:String,
required:true,
},
disabled:{
type:Boolean,
default:false,
},
theme:{
type:String,
required: true,
}
},
data(){
return {
a:false
}
},
methods:{
text:true
}
}
</script>
<style lang="scss" scoped>
.check-box{
display: block;
height: 24px;
user-select: none;
.label{
position: relative;
cursor: pointer;
display: block;
overflow: hidden;
height: 24px;
width: 80px;
display: block;
border: solid 1px #ccc;
border-radius: 3px;
.input{
position: absolute;
z-index: -1;
}
.text-box{
position: absolute;
z-index: 3;
right: 0px;
top: 0px;
width: 57px;
height: 24px;
line-height: 24px;
text-align: center;
.uncheck-text{
font-size: 12px;
color: #666;
}
.checked-text{
font-size: 12px;
color: #fff;
}
}
.bg-box{
position: absolute;
z-index: 1;
left: 0px;
top: 0px;
transition: all 0.35s;
width: 160px;
height: 24px;
.uncheck{
float: left;
width: 80px;
height: inherit;
background-color: #fff;
}
.checked{
float: left;
width: 80px;
height: inherit;
background-color: #0099FF;
}
}
.bg-box-active{
left: -80px;
}
.frame-box{
position: absolute;
z-index: 2;
top: 3px;
left: 3px;
height: 18px;
width: 18px;
box-sizing: border-box;
border: solid 1px #ccc;
background-color: #fff;
border-radius: 3px;
text-align: center;
.iconfont{
font-size: 12px;
color: #fff;
line-height: 16px;
}
}
}
.checked{
.frame-box{
.iconfont{
color: #0099FF;
}
}
}
.disabled{
border-color: #CCC;
.frame-box{
.iconfont{
color: #D7D7D7 !important;
}
}
.bg-box{
.uncheck{
background-color: #D7D7D7 !important;
}
.checked{
background-color: #D7D7D7 !important;
}
}
.text-box{
.uncheck-text{
color: #fff !important;
}
.checked-text{
color: #fff !important;
}
}
}
}
.theme-1{
.checked{
.frame-box{
.iconfont{
color: #0099FF;
}
}
}
.label{
.bg-box{
.checked{
background-color: #0099FF;
}
}
}
}
.theme-2{
.checked{
.frame-box{
.iconfont{
color: #C23031;
}
}
}
.label{
.bg-box{
.checked{
background-color: #C23031;
}
}
}
}
.theme-3{
.checked{
.frame-box{
.iconfont{
color: #FFAF7A;
}
}
}
.label{
.bg-box{
.checked{
background-color: #FFAF7A;
}
}
}
}
</style>
结语
以上就是个人对于Vue中的v-model指令的一点心得,文章有疑问或错误的地方还请指出,感谢阅读。
止水
于沈阳
2019/10/24 09:13