如何正确理解和使用Vue中的v-model指令

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指令,做完的效果如下图:


自定义复选框.png

绝大多数情况下,我们会将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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • 主要还是自己看的,所有内容来自官方文档。 介绍 Vue.js 是什么 Vue (读音 /vjuː/,类似于 vie...
    Leonzai阅读 3,324评论 0 25
  • 一、了解Vue.js 1.1.1 Vue.js是什么? 简单小巧、渐进式、功能强大的技术栈 1.1.2 为什么学习...
    蔡华鹏阅读 3,311评论 0 3
  • 什么是组件? 组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装...
    youins阅读 9,449评论 0 13
  • # 在本文中,笔者又提炼了以下几个重点 补偿双向数据绑定 Vue.$set 数据侦听 Vue.$watch 表单绑...
    果汁凉茶丶阅读 1,459评论 1 15
  • Vue 实例 属性和方法 每个 Vue 实例都会代理其 data 对象里所有的属性:var data = { a:...
    云之外阅读 2,198评论 0 6