【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 为它们做了单独的适配:

  • texttextarea 元素使用 value 属性和 input 事件;
  • checkboxradio 使用 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,这样是否可行呢?
不妨做一个实验:
多层嵌套的v-model
我们分别为三个组件都准备一个值显示器调节手柄
App中,调节手柄的主要工作为直接修改值:

  methods:{
	  changeValue(dv){
		 this.parentValue += dv;
	  }
  }

而在ParentChild 两个子组件中,调节手柄的作用与第二部分的自定义子组件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>

现在我们要做的,就是实现myNumnum的同步。
当下层组件给parent传值时,myNum中的值会发生变化,我们通过watch监听变化,并将变化传递给上层组件,由上层组件修改num的值(从而避免直接修改props):

	myNum:{
		handler(newValue,oldValue) {
			this.$emit("numChanged",newValue)
		}
	}

同时,当上层组件的num发生变化时,我们也需要同步myNum的值:

	num:{
		handler(newValue,oldValue) {
			this.myNum = this.num;
		}
	},

基本完成了,但还有一个注意点。在Vue组件完成挂载时,myNumnum的值可能不同步,这并不会被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。但我们为了实现这个功能,增加了datawatchmounted三个字段,确实令人困惑。
事实上,可以只使用一个计算属性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="female">
				<input type="radio" id="female" value="" v-model="gender"></label>
			<h2>您选择的性别是:{{gender}}</h2>
		</div>
	</div>
</template>

2. 修饰符

在使用 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 技巧!

  • 7
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

没头发的米糊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值