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

  • 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,这样是否可行呢?
不妨做一个实验:

我们分别为三个组件都准备一个值显示器调节手柄
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=以上是关于Vue关于 v-model,你想要的全部知识点,都在这儿了的主要内容,如果未能解决你的问题,请参考以下文章

关于在VUE中下拉列表的option无法添加事件传值的问题

Vue 进阶教程之:详解 v-model

vue v-model 的实现原理

vue v-model 的实现原理

vue v-model 的实现原理

关于vue3中v-model做了哪些升级