后面有完整的调试代码,可以先运行,然后再结合文章容易理解。
v-model这个指令究竟是怎么回事?这篇写下我的理解,看和大家的想法是否一样。
这篇文章写的比较长,但是知识点学一个少一个,从思想上而不是从代码上去理解一个东西我觉得比较重大。有兴趣的可以精读这篇文章。
v-model的核心是强调动作
举个例子,A是老师,B是学生,A对B说,你考试完成之后,在我给你的表中签个名。这句话涉及到两个动作。1、考试完成的动作、2、签名的动作。
签名动作是我方提供的,考试完成动作是对方的,对方动作发生时触发我方动作。理解了这个简例,再看<input>控件的v-model。
<div id="app">
<input type="text" v-model="username" />
..
</div>
看到这个代码时,我们就想到书本是这样写的:<input>发生变化,那么username就发生变化。我认为这个说法是在现象上解释,不会让人印象深刻,我的理解是这样的:我方提供一个变量给对方,对方发生某个动作时修改我方变量,怎么修改呢?就是我方提供一个动作。
我没有说对方是哪个动作,它是哪个动作我也不知道。对于我方来说,对方是一个黑盒。我只能要求vue在对方发生某个事件时触发我方提供的动作。

那么也有人会疑问,input变化时的确 赋值给了username。只能说一切都是碰巧,就好比它是原理之下的一个现象展示。由于<input type=”text”>这个控件频率最高的事件是input,而直接赋值给username是vue默认这样做的,也就是说它替我们安排了。
所以我们需要研究两个问题,第一个问题是控件会发生什么动作,第二个问题是我方提供什么动作。
第一:input控件会发生什么动作?
它可能会发生任意的动作,我们也不知道。这就说明一个问题,对方的动作是因对方的性质不同而不同。列如说,当前是input text控件,它最常用的两个动作就是input(输入时)和change(丢失焦点时),但除此之外,还有checkbox,radio等等各种不同的控件,所以动作也是不同的。
因此这里Vue做了一个假设,它认为<input> text控件的动作就是input事件,由于这是频率最高的。但是不同的控件,动作也是不同的。此时,Vue就起了作用,它将这些差别屏蔽了。
第二:我方的动作是什么?
从代码中,我们没有看到我方提供的动作,这是最让人疑惑的地方。那么vue就在内部生成一个动作,也就是一个函数,这个是问题的真正核心,也就是说Vue帮我们生成一个函数,伪码如下:
//伪码
with(this){
f = function($event){
..
username=$event.target.value
}
}
那么最终就相当于
//伪码
input控件.input回调 = [
f,
....
]
<input>控件发生input回调时,就会调用我方提供的一个动作函数。这里为什么用数组,是由于回调可以有任意个。
目前的问题是,为什么我方要有这个函数动作呢?
仅仅由于一个问题:那就是对方如何修改我方的数据?在JS中,使用绑定this作用域的闭包函数是最合适的方法。如果不绑定一个当前this,回调执行时无法解析username是谁的。
实则说的是js的一个绑定特性,如果我们用with(this)绑定返回一个闭包函数时,无论这个函数后来放到哪里,它内部的this就和原来绑定的this一样。
完整的故事流程
通过解析,我们还原了整个故事的真相。
- 我方生成一个函数(也就是Vue替我们生成的),并用with(this)绑定作用域。
- 将这个函数添加到<input>控件的input事件回调中。
- input事件发生时,就会传递$event事件对象,然后执行我方的函数,在内部将我方的username做了修改,主要是赋值。
- username修改之后触发R-Watcher执行更新,就会更新页面引用的地方,实现双向绑定。
这个例子很简单,我认为起了一个抛砖引玉的作用,主要是说明v-model到底是啥?
公式:f(对方动作)->(调用) f(我方动作)-> 修改v-model指定的变量
下面看下给组件传v-model。
组件传递v-model
第一看个图示:

为什么前面举input的例子,就是做类比。我们把当前的这个子组件看作是<input>控件,这样逻辑上能通顺,类比法是一个超级好的方法!我们把v-model传给子组件,根据原理我们是这样想的:
子组件发生某个动作时执行我方的动作函数,在其中将我方的值进行修改,这样就实现父子双向绑定了,也就是完全类比上面的<input>控件。
对方的动作
我们先解决第一个问题就是对方的动作。但此时物是人非,东西都已经不一样了。由于它是组件不是控件,因此就不存在天然的input、change等事件。
那么怎么办呢?Vue是这样想的,它把我方的动作放在子组件的events,也就是事件中,然后在子组件执行动作时,使用$emit执行我方的事件,那么就相当于<input>控件中执行input回调。
思路清楚之后再看子组件的动作,那么就无规定了,它想做什么动作就做什么动作,反正只要使用$emit执行我方的事件就可以了。
我方的动作
我方和前面的例子一样,只是简单的给了一个v-model,那么vue同样的也在内部生成一个函数。但是又由于要放到子组件的events中,那么总要给它起个名字,不然子组件$emit找不到名字执行。vue认为默认就和<input>一样,就将此事件名起为input,如下所示:

事件的问题解决了,那么v-model传递的变量的值怎么解决呢?
对方提供prop接收
对方要有一个变量存储我方的值,不然没法引用。但又由于组件和控件不一样,列如说<input type=”text”>控件天然就有一个value属性,所以直接放到里面,组件没有那么就需要专门有个变量存储这个值,Vue在内部默认名为value,如下:

value这个名字是Vue默认起的,它专门存放传递过来的v-model的值。vue2.x只能向子组件传递一个v-model,所以也不会乱。在vue3.x中可以绑定多个v-model,在这里又体现了原理的重大性,如果你不理解这一层,就不理解为什么vue3.x要实现绑定多个v-model,由于我们必定会想到能不能传多个v-model。
有了这个prop,那么子组件就可以对它进行操作,完成之后将结果再返回给我方的事件,那么就将我方的值修改了,实现了双向绑定。
问题出现:prop不用value可以吗?
Vue默认规定了’value’的prop名字,如果说我们原来已经定义了,或者说就是不想让它用这个名字可以吗?在Vue中也想到这个问题,它有一个model配置,可以同时修改prop和事件名:
<div id="app">
<p>状态: {{ isActive ? '激活' : '未激活' }}</p>
<!-- 仍用 v-model 绑定,但实际触发的是自定义事件 -->
<custom-component v-model="isActive"></custom-component>
</div>
<script src="../vue.js"></script>
<script>
Vue.component('custom-component', {
// 自定义 v-model 的配置
model: {
prop: 'checked', // 接收值的 prop 名(默认是 value)
event: 'change' // 触发更新的事件名(默认是 input)
},
// 对应接收的 prop
props: ['checked'],
template: `
<button @click="handleClick">
{{ checked ? '点击关闭' : '点击开启' }}
</button>
`,
methods: {
handleClick() {
// 触发自定义的 change 事件,而不是默认的 input 事件
this.$emit('change', !this.checked);
}
}
});
new Vue({
el: '#app',
data() {
return { isActive: false }
}
});
</script>
大家看到这里有没有感觉,只要理解了原理,再看流程就会像河水流动一样自不过然的发生。
子组件传递v-model的总结
当父组件向子组件传递v-model时,等价于传了一个prop value和一个事件input,子组件发生某个动作时就先进行运算,得到一个结果,然后调用事件将结果传递过去,进而修改v-model中绑定的变量,实现双向绑定。
它是一个语法糖的功能,用$emit调用父组件事件也一样可以完成,但是我们更主要说明的是,v-model究竟是什么。
v-model双向绑定的作用
有时操作第三方组件时,我们需要知道第三方组件修改了我们传递过去的值,并且想得到通知时,v-model很有用处。
实验:子组件发生事件通知父组件
这个例子用组件模拟控件的使用,父组件传递一个值,子组件发生变化时反向修改父组件的值,实现双向绑定。
<!DOCTYPE html>
<html>
<head>
<script src="../vue.js"></script>
<style>
.switch {
width: 60px;
height: 30px;
background: #ccc;
border-radius: 15px;
position: relative;
cursor: pointer;
}
.switch.active {
background: #4cd964;
}
.switch::after {
content: '';
position: absolute;
width: 26px;
height: 26px;
border-radius: 50%;
background: white;
top: 2px;
left: 2px;
transition: left 0.3s;
}
.switch.active::after {
left: 32px;
}
</style>
</head>
<body>
<div id="app">
<p>开关状态: {{ isOn ? '开启' : '关闭' }}</p>
<!-- 用 v-model 绑定自定义开关 -->
<custom-switch v-model="isOn"></custom-switch>
</div>
<script>
Vue.component('custom-switch', {
// 子组件通过 value 接收父组件的值
props: ['value'],
template: `
<!-- 点击时修改状态,并同步给父组件 -->
<div
class="switch"
:class="{ active: value }"
@click="$emit('input', !value)"
></div>
`
});
new Vue({
el: '#app',
data() {
return { isOn: false }
}
});
</script>
</body>
</html>



