2
写文章不容易,点个赞呗兄弟
专注 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

看完结论,有点懵?我们来看看具体的内容,结果导向来进行学习

下面的讲解以下面这个为例

image

image

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

image

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 执行的时候,会打印出什么呢?行了,给你看结果了

image

好吧,再一次深刻认识到一个知识点,with 绑定作用域原来这么强,离开with执行,还是先访问他的作用域,脱离不出魔爪啊,强盗作用域

image

回来总结

于是当事件回调执行的时候,会 直接赋值 给 组件实例的name,这样便通过外部改变了内部数据

TIP

外部变化,本来可能会存在一种情况

a、手动修改表单后, 回调内会更新组件的值

b、组件的值更新之后,会通知组件更新,组件更新时,便又会重新把input 赋值一遍

非常多余的一步操作,所以这里,Vue做一个判断,判断旧值和 新值是否相等,不等才更新,关于旧值,会保存在 dom 的 _value 属性


总结

v-model 三要素

1、绑定属性

2、绑定事件

3、属性+事件组合完成双向更新

公众号


神仙朱
235 声望105 粉丝

不会认输