v-model
的使用原理在 Vue 中,使用 v-bind
可以实现单向数据流,即父组件向子组件传入基本数据类型,但反过来,子组件中不能修改父组件传过来的基本数据类型。
想要实现数据的双向传递,需要使用 Vue 提供的事件机制。即在子组件中通过 $emit()
触发一个事件,在父组件中则需要使用对应的 v-on
属性监听对应的事件,并在事件发生时修改相应的数据。
Vue 将其简化为了一个语法糖,即:
<input type="text" v-model="name">
本质上是:
<input type="text" :value="name" @input="name = $event.target.value">
而根据 Html 5 标准,对于、
、
等原生的表单标签,它们的属性不一定都是
value
,发出的事件也不一定都是 input
。因此,Vue 为它们做了单独的适配:
text
和 textarea
元素使用 value
属性和 input
事件;checkbox
和 radio
使用 checked
属性和 change
事件;select
字段将 value
作为 prop
并将 change
作为事件。但对于除这些标签以外的其他标签,Vue 默认 会 绑定 value
属性 和 监听 input
事件。
基于此,只需要记住一个事实,v-model
只是同时完成 数据的绑定 和 事件的监听 而已,它内部实现的机理只是一个简化书写的语法糖。
v-model
了解了 v-model
的原理,我们可以想象,想要在自定义组件中实现 v-model
,其实对应要做的就是 允许父组件进行数据绑定 和 在数据发生变化时发出对应的事件 即可。
既然对组件使用 v-model
时,Vue 默认 会 绑定 value
属性 和 监听 input
事件。那么我们就可以依靠拼凑语法糖的方式在自定义组件上实现 v-model
。
首先,我们拥有一个父组件 App.vue
,其中包含一个子组件 Parent
。它要实现的功能是一个带有调节按钮的数值选择器:
父组件
App
的参考代码:
<template>
<div id="app">
<Parent v-model="parentValue">Parent>
div>
template>
<script>
import Parent from './components/Parent.vue'
export default {
name: 'app',
data(){
return {
parentValue:5
}
},
components:{
Parent
}
}
script>
现在,问题就只剩下如何在子组件Parent
中拼凑出v-model
的语法糖。
子组件的结构如下:
<template>
<div id="Parent">
<button @click="changeValue(-1)"> - button>
{
{value}}
<button @click="changeValue(+1)"> + button>
div>
template>
因为 Vue 会 默认绑定 value
属性,因此我们在子组件的 props
中添加 value
字段。
props:{
value:{
type: Number,
default: 5
}
},
因为 Vue 会 默认监听 input
事件,因此在改变数值时,应当发出 input
事件。同时在事件中包裹新的值,以便父组件接收。
methods:{
changeValue(dv) {
this.$emit("input",this.value + dv)
}
}
通过以上简单的两步,我们就轻松地拼凑出了 v-model
的默认语法糖,从而实现了自定义组件中的 v-model
。
子组件
Parent
的参考代码:
<template>
<div id="Parent">
<button @click="changeValue(-1)"> - button>
{
{value}}
<button @click="changeValue(+1)"> + button>
div>
template>
<script>
export default{
name:"Parent",
props:{
value:{
type: Number,
default: 5
}
},
methods:{
changeValue(dv) {
this.$emit("input",this.value + dv)
}
}
}
script>
model
字段拼凑默认的语法糖虽然可行,但显然这并不是一种理想的方式。因为我们想要实现v-model
的字段不一定是value
,如何为v-model
自定义绑定属性和监听事件呢?
假设我们现在不使用 value
属性和 input
事件,而是使用了名为 num
的属性 和 名为 numChanged
的事件。
新的子组件
Parent
的参考代码 - 1:
<template>
<div id="Parent">
<button @click="changeValue(-1)"> - button>
{
{num}}
<button @click="changeValue(+1)"> + button>
div>
template>
<script>
export default{
name:"Parent",
props:{
num:{
type: Number,
default: 5
}
},
methods:{
changeValue(dv) {
this.$emit("numChanged",this.num + dv)
}
}
}
script>
至此,默认的v-model
就失效了。
这时候就需要请出我们的model
字段(仅限于 Vue 2.2.0+)。
model:{
prop:"num",
event:"numChanged"
},
在子组件中添加如上的代码段,就可以自定义 v-model
的 属性 和 监听事件。
如上,我们就通过使用 model
字段完成了自定义组件的 v-model
。
新的子组件
Parent
的参考代码 - 2:
<template>
<div id="Parent">
<button @click="changeValue(-1)"> - button>
{
{num}}
<button @click="changeValue(+1)"> + button>
div>
template>
<script>
export default{
name:"Parent",
model:{
prop:"num",
event:"numChanged"
},
props:{
num:{
type: Number,
default: 5
}
},
methods:{
changeValue(dv) {
this.$emit("numChanged",this.num + dv)
}
}
}
script>
v-model
现在描述这样一个情形:假设我有超组件App
、父组件Parent
和子组件Child
。我需要通过v-model
将App中的一个值 parentValue
一路绑定至 Child
,并实现同步。
假设直接使用刚才的方式,分别为Parent
组件和Child
组件实现自定义的v-model
,这样是否可行呢?
不妨做一个实验:
我们分别为三个组件都准备一个值显示器和调节手柄。
在App
中,调节手柄的主要工作为直接修改值:
methods:{
changeValue(dv){
this.parentValue += dv;
}
}
而在Parent
和 Child
两个子组件中,调节手柄的作用与第二部分的自定义子组件v-model
一致:
methods:{
changeValue(dv) {
this.$emit("numChanged",this.num + dv)
}
},
在这样的状况下,我们做下面三个操作:
App
的调节手柄时,值的变化成功同步到了两个子组件中。Parent
的调节手柄时,值的变化也成功同步到了它的父组件和它的子组件中。Child
的调节手柄时,出现了问题。Parent
,App中的值没有发生变化。v-model
的实现原理上。Child
的调节手柄时,它会向父组件Parent
发出一个事件:this.$emit("numChanged",this.num + dv)
此时,父组件因为绑定了v-model
,所以会接收这个事件,而v-model
是如下语句的语法糖:
<child :num="num" @numChanged="num = $event">child>
而上述语句num = $event
修改了 prop
中属性的值,这违背了Vue的设计原则。同时parent
组件也没有再进一步向父组件App
发出事件,导致值的修改没有被同步到App
。
这个问题该如何解决?
很多朋友可能会首先想到,通过为parent
组件中的值设定watch
监听器,在值变化时,向父组件App发出事件,完成同步。
watch:{
num:{
handler(newValue,oldValue) {
this.$emit("numChanged",newValue)
}
}
},
这样确实实现了功能。因为如此推断,当num
发生变化时,parent
确实能够通过事件将变化传给上一级。
但这么写并不优雅。因为在底层,仍然是首先修改了props
的值,然后才通知父组件修改相应的值。这仍然会引发Vue的警告。
考虑v-model
的底层实现机制,直接向下层组件通过v-model
传递prop
中的值,必然会引发赋值。因此,在v-model
中必须传递一个非props
值。
data(){
return {
myNum:5
}
},
我们在parent
组件内安排一个新的data
属性myNum
,并把它绑定给v-model
:
<template>
<div id="Parent">
Parent 中目前的值: {
{num}}
<br>
Parent 的调节手柄:<button @click="changeValue(-1)"> - button>
<button @click="changeValue(+1)"> + button>
<child v-model="myNum">child>
div>
template>
现在我们要做的,就是实现myNum
和num
的同步。
当下层组件给parent
传值时,myNum
中的值会发生变化,我们通过watch
监听变化,并将变化传递给上层组件,由上层组件修改num
的值(从而避免直接修改props
):
myNum:{
handler(newValue,oldValue) {
this.$emit("numChanged",newValue)
}
}
同时,当上层组件的num
发生变化时,我们也需要同步myNum
的值:
num:{
handler(newValue,oldValue) {
this.myNum = this.num;
}
},
基本完成了,但还有一个注意点。在Vue组件完成挂载时,myNum
和num
的值可能不同步,这并不会被watch
监听到,因此我们还需要提供一个钩子:
mounted(){
this.myNum = this.num
},
通过这一系列操作,我们用一个data
属性代替了props
属性,从而避免了警告。
新的子组件
Parent
的参考代码 - 3:
<template>
<div id="Parent">
Parent 中目前的值: {
{num}}
<br>
Parent 的调节手柄:<button @click="changeValue(-1)"> - button>
<button @click="changeValue(+1)"> + button>
<child v-model="myNum">child>
div>
template>
<script>
import child from './Child.vue'
export default{
name:"Parent",
model:{
prop:"num",
event:"numChanged"
},
props:{
num:{
type: Number,
default: 5
}
},
data(){
return {
myNum:5
}
},
methods:{
changeValue(dv) {
this.$emit("numChanged",this.num + dv)
}
},
watch:{
num:{
handler(newValue,oldValue) {
this.myNum = this.num;
}
},
myNum:{
handler(newValue,oldValue) {
this.$emit("numChanged",newValue)
}
}
},
mounted(){
this.myNum = this.num
},
components:{
child
}
}
script>
在上一个解决方案中,我们选择使用watch
的监听方法,用一个data
代替prop
。但我们为了实现这个功能,增加了data
、watch
和mounted
三个字段,确实令人困惑。
事实上,可以只使用一个计算属性computed
,来完成实现这些功能:
computed:{
myNum:{
get(){
return this.num;
},
set(newValue){
this.$emit("numChanged",newValue)
}
}
},
新的子组件
Parent
的参考代码 - 4:
<template>
<div id="Parent">
Parent 中目前的值: {
{num}}
<br>
Parent 的调节手柄:<button @click="changeValue(-1)"> - button>
<button @click="changeValue(+1)"> + button>
<child v-model="myNum">child>
div>
template>
<script>
import child from './Child.vue'
export default{
name:"Parent",
model:{
prop:"num",
event:"numChanged"
},
props:{
num:{
type: Number,
default: 5
}
},
methods:{
changeValue(dv) {
this.$emit("numChanged",this.num + dv)
}
},
computed:{
myNum:{
get(){
return this.num;
},
set(newValue){
this.$emit("numChanged",newValue)
}
}
},
components:{
child
}
}
script>
这一部分内容中,我们介绍了如何在在多层嵌套的组件中使用v-model
。在这种情况下,除了最顶层组件和最底层组件,其他的中间组件都需要使用data
代替props
。我们推荐使用解决方案2,因为它比较优雅且易读。
v-model
的一些其他使用细节
当我们为多选的 select
标签绑定 v-model
时,得到的会是一个数组。
复选的 checkbox
同理。
多选的
select
的参考代码:
<template>
<div id="app">
{
{mySelects}}
<select v-model="mySelects" multiple>
<option value="apple">苹果option>
<option value="banana">香蕉option>
<option value="orange">橘子option>
select>
div>
template>
但当我们为多个单选的radio标签绑定v-model时,得到的只是单选的值。
单选的
radio
的参考代码:
<template>
<div id="app">
<div id="app">
<label for="male">
<input type="radio" id="male" value="男" v-model="gender"> 男
label>
<label for="female">
<input type="radio" id="female" value="女" v-model="gender"> 女
label>
<h2>您选择的性别是:{
{gender}}h2>
div>
div>
template>
在使用 v-model
时,在后面加上 “.[修饰符]
” ,即可实现一些特殊的功能。
lazy
修饰符:v-model
默认是在input
事件中同步输入框的数据的。data
中的数据就会自动发生改变。lazy
修饰符可以让数据在失去焦点或者回车时才会更新:number
修饰符:number
修饰符可以让在输入框中输入的内容自动转成数字类型。trim
修饰符:trim
修饰符可以过滤内容左右两边的空格。修饰符
lazy
的参考代码:
<div id="app">
<input type="text" v-model.lazy="content" placeholder="请输入">
<p>输入框:{
{content}}p>
div>
<script>
new Vue({
el: '#app',
data: {
name: '123',
content: ''
}
})
script>
v-model
作为一个语法糖,在 Vue 中有着非常重要的地位。合理、有效地使用 v-model
可以有效提升项目代码的可读性。
本专栏将持续更新,关注我,继续带你体验更多 Vue 技巧!