Vue关于 v-model,你想要的全部知识点,都在这儿了
Posted 没头发的米糊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue关于 v-model,你想要的全部知识点,都在这儿了相关的知识,希望对你有一定的参考价值。
一、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 标准,对于<input>
、<textarea>
、<select>
等原生的表单标签,它们的属性不一定都是 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
,其实对应要做的就是 允许父组件进行数据绑定 和 在数据发生变化时发出对应的事件 即可。
方法1. 拼凑默认的语法糖
既然对组件使用 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>
方法2. 使用 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的警告。
完美的解决方案 1:
考虑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>
完美的解决方案 2:
在上一个解决方案中,我们选择使用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
的一些其他使用细节
1. 多选与单选
当我们为多选的 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=以上是关于Vue关于 v-model,你想要的全部知识点,都在这儿了的主要内容,如果未能解决你的问题,请参考以下文章