写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue版本 【2.5.17】
如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧
【Vue原理】VModel - 白话版
说到 Vue,感觉第一印象就是双向绑定,所以v-model键值是Vue印象的半壁江山啊,这么重要的东西,你好歹要知道是怎么实现的吧
我们今天就来讲解双向绑定的工作原理,你应该知道,双向绑定就是通过绑定 dom 事件来实现的,可是,怎么绑定的事件,绑定什么事件?
首先,双向绑定,我个人认为应该分为 初始化绑定 和 双向更新 两part。
初始化绑定,就是初始化时给表单元素绑定值,绑定事件,为双向更新做准备
双向更新,就是任意一边变化,同时能让另一个边更新
双向更新那是后话,只有一开始时成功执行绑定操作才会有之后 双向更新这个东西,所以,今天按顺序来了解两个part,从四个问题开始
1、v-model 怎么给表单绑定数据
2、v-model 绑定什么事件
3、v-model 怎么绑定事件
4、v-model 如何进行双向更新
TIP
v-model 还可以用在 自定义组件上,但是很明显,这次我们先不讲这一块,而是先将正常的表单使用
先看结论
我们先以 input text 类型讲解,对于其他的表单元素,流程都差不多,只是中间涉及的内容不同。所以就先讲个例子,然后具体在源码版全部一起说
1、怎么赋值?v-model 绑定的数据赋值给表单元素的 value 属性
2、怎么绑定事件?解析不同表单元素,配置相应的事件名和事件回调,在插入dom之前,addEventListener 绑定上事件
3、怎么双绑?外部变化,触发事件回调,event.target.value 赋值给model绑定的数据;内部变化,修改表单元素属性 value
看完结论,有点懵?我们来看看具体的内容,结果导向来进行学习
下面的讲解以下面这个为例
v-model 怎么给表单绑定数据
获取值流程
首先,上面例子解析后的渲染函数是下面这样(已简化,只保留表单值相关)
(function(){
with(this){
return _c('div',[
_c('input',
domProps:{"value":name}
)
])
}
})
1、这个渲染函数是没有执行的 匿名函数。执行的时候,会绑定上下文对象为 组件实例
2、于是 with(this) 中的 this 就能取到 组件实例本身,with 的代码块 顶层作用域 就绑定为了 组件实例
3、于是 with 内部变量的访问,就会首先访问到 组件实例上。其中 name 的 获取,就会先从 组件实例上获取,相当于 vm.name。赋值完成后,domProps 就是下面这样
{ domProps:{value:111} }
4、上面的 value 是 v-model 解析成的原生属性,保存在属于该节点 input 的 domProps 对象存储器中
绑定值流程
创建dom input 之后,插入dom input 之前,遍历该 input 的 domProps ,逐个添加给 input dom
简化后的代码像下面这样进行赋值
for(var i in domProps){
input[i]=domProps[i]
}
其中给节点赋值就是这样
input.value = 111
v-model 绑定什么事件
不同的表单元素使用v-model,会绑定不同的 事件
change 事件
select,checkbox,radio
input 事件
这是默认事件,当不是上面三种表单元素时,会解析成 input 事件
比如 text、number 等 input 元素和 textarea
TIP
实际上关于 input 和 textarea 绑定的事件远不止 input 一个事件,但是涉及内容太多,打算放在源码版说明,这里先简单默认是 input 事件
v-model 怎么绑定事件
上面例子解析成下面的渲染函数(已简化,只保留事件相关)
with(this) {
return _c('div', [
_c('input', {
on: {
"input": function($event) {
name = $event.target.value
}
}
}
)])
}
解析事件流程
1解析不同表单元素,配置不同的事件
2拼装 事件回调函数,不同表单元素,回调不一样
3把 事件名和拼装回调 配套 保存给相应的表单元素的 on 事件存储器
什么时候绑定事件
生成 input dom 之后,插入input dom 之前
怎么绑
使用之前保存的 事件名和 事件回调,给 input dom 像下面这样绑定上事件
input.addEventListener("input",function($event) {
name = $event.target.value
})
v-model 如何进行双向更新
双向,指的是 外部和内部
外部变化:用户手动改变表单值,输入或者选择
内部变化:从内部修改绑定值
内部变化
1、v-model 绑定了 name ,name 会收集到 本组件的 watcher
a. 下面渲染函数执行时,会绑定本身组件实例为上下文对象
b. name 访问的是 组件实例的 name
c. name 此时便收集到了 当前正在渲染的组件实例,当前渲染的实例是自己,于是收集到了自身的 watcher
(function(){
with(this){
return _c('div',[_c('input',
domProps:{"value":(name)})
])
}
})
如果你不懂 收集 watcher 什么意思,建议看下我的另一篇文章
【Vue原理】响应式原理 - 白话版
2、内部修改 name 变化,通知收集器内的 watcher 更新,所以本组件会更新,上面的渲染函数重新执行,便 重新从实例读取 name
3、重新给 input dom 的 value 赋值,于是 页面就更新了
怎么赋值?那是回到第一个问题了,兄弟
外部变化
手动改变表单,事件触发,执行事件回调(下面这个),更新组件数据 name
function($event) {
name = $event.target.value
}
更新内部数据流程
1、当事件触发的时候,会把 表单的值 赋值给 name
2、name 是从 组件实例上访问的
3、所以这次赋值会 直接改变组件实例的 name
回调怎么赋值给组件实例的name
一开始不懂,所以不理解,也没查到,写了个例子,大概理解了意思
1、因为事件回调 在 with 里面声明
2、于是事件回调的 作用域链最顶层 就加上了一层 with 绑定的作用域
3、就算事件回调不在 with 中执行,事件回调中的 变量访问,也会先访问之前 with 绑定过的作用域,因为作用域链的最顶层
with举栗子
var name=22
var a={name:"a"}
with(a){
var fn=function(){
console.log(name)
}
}
fn()
你认为 fn 执行的时候,会打印出什么呢?行了,给你看结果了
好吧,再一次深刻认识到一个知识点,with 绑定作用域原来这么强,离开with执行,还是先访问他的作用域,脱离不出魔爪啊,强盗作用域
回来总结!
于是当事件回调执行的时候,会 直接赋值 给 组件实例的name,这样便通过外部改变了内部数据
TIP
外部变化,本来可能会存在一种情况
a、手动修改表单后, 回调内会更新组件的值
b、组件的值更新之后,会通知组件更新,组件更新时,便又会重新把input 赋值一遍
非常多余的一步操作,所以这里,Vue做一个判断,判断旧值和 新值是否相等,不等才更新,关于旧值,会保存在 dom 的 _value 属性
总结
v-model 三要素
1、绑定属性
2、绑定事件
3、属性+事件组合完成双向更新