【Vue原理】VModel - 源码版 之 表单元素绑定流程

↑点击  “悲伤日记”  持续关注我

专注 Vue 源码分享,为了方便大家理解,分为了白话版和 源码版,白话版让大家可以轻松理解工作原理,源码版让大家更清楚内部操作和 Vue的美,喜欢我就关注我,好吧兄弟,不会让你失望的

今天讲解 v-model 的源码版。首先,兄弟,容我先说几句

v-model 涉及源码很多,篇幅很长,我都已经分了上下  三篇了,依然这么长,但是其实内容都差不多一样,但是我还是毫无保留地给你了。你知道我这篇文章写了多久,一个多星期啊,不是研究多久啊,是写啊写啊,不停地修修改改,一直在想如何才能讲明白

如果你做好了十足的学习准备,会对你事半功倍,如果你只是看看,请看白话版吧,不然估计会越看越烦.....

如果你看过白话版,估计你会了解今天内容的大概,也能很快就入戏

今天讲解不同表单元素的Vue是如何处理的,表单元素有 

inputtextareaselectcheckboxradio 五大种

所以,我们把每个表单元素当做一个模块,然后每个模块解决三个问题的流程,来开始我们今天的表演

1、v-model 如何绑定表单值

2、v-model 如何绑定事件

4、v-model 如何双向更新

TIP

下面所有涉及到的源码,为了方便理解,都是简化过的,因为源码太长,所以只保留主要思想,去掉了很多兼容处理以及错误处理

v-model 指令的处理

我们现在假设模板的解析已经到了 解析 v-model 的部分....

Vue 会调用 model 方法 来解析 v-model ,这个方法里面,针对不同的表单元素,再调用不同的专属方法进行深度解析

function model(el, dir) {    
    var value = dir.value;     
    var tag = el.tag;    
    var type = el.attrsMap.type;    
    if (tag === 'select') {
        genSelect(el, value);    }
    else if (tag === 'input' && type === 'checkbox') {
        genCheckboxModel(el, value);    }
    else if (tag === 'input' && type === 'radio') {
        genRadioModel(el, value);    }
    else if (tag === 'input' || tag === 'textarea') {
        genDefaultModel(el, value);    }}

你也看到了,上面每种表单元素都会使用一个方法来特殊照顾,不过这些方法,作用大致一样

1给表单元素设置绑定值

2给表单元素设置事件及回调

所以这里,我们把方法的都设计到的方法以及流程说一下

插播上面的el 是什么?

el 是 ast,而我的理解就是解析模板后,用树结构来表示某个dom节点,这里先不用深究,你就只要知道他是保存解析模板后所有的数据,包括你绑定的事件,绑定的指令,绑定的属性等等,一张图看下

下面所有的处理都是以 el 为基础的

表单元素设置绑定值

什么叫设置绑定值?

首先,比如你给表单元素设置 v-model ="name",name 是 内部数据吧,所以要把 name 和 表单元素 两个紧紧绑定起来,方便后面进行双向更新

 里讲的是每个表单元素绑定值的流程

他们都会调用 addProp 去保存绑定的属性 然后 绑定属性,流程一样,所以提出来讲,但是具体绑定什么属性,每种元素都不尽相同,在下面表单元素模块会详解

1调用 addProp,把 value 添加进 el.props

function addProp(el, name, value) {    (el.props || (el.props = [])).push({ name: name, value: value });}

2 接下来的解析,el.props 会拼接成进字符串 domProps

function genData$2(el, state) {    
    var data = '{';    
    if (el.props) {
        data += "domProps:{" + (genProps(el.props)) + "},";    }    data = data.replace(/,$/, '') + '}';    
    return data
}

3在插入 dom 之前,调用 updateDOMProps ,把 上面保存的 domProps 遍历赋值到 dom 上

function updateDOMProps(oldVnode, vnode) {    
    var props = vnode.data.domProps || {};    
    for (key in props) {
        cur = props[key];        
        if (key === 'value') {
            elm._value = cur;            elm.value = strCur;        }
        else {
            elm[key] = cur;        }    }}

表单元素设置事件以及回调

 这里讲的是每个表单元素绑定事件的流程

1拼接事件

每种元素拼接事件都不一样,在下面表单元素模块会详解

2保存事件名和拼接好的回调

每个元素的 event 事件 和 拼接的回调是不一样,但是他们保存的流程都是一样的,都会调用下面的方法,addHandler 去保存事件

下面 el 是dom 元素,event 是事件名,code 是拼接的回调

function addHandler(el, name, value) {    
    var events = el.events || (el.events = {});    
    var newHandler = {        
        value: value.trim()
    };    
    var handlers = events[name];    
    if (Array.isArray(handlers)) {
        important ? handlers.unshift(newHandler) : handlers.push(newHandler);    }
    else if (handlers) {
        events[name] = important ? [newHandler, handlers] : [handlers, newHandler];    }
    else {
        events[name] = newHandler;    }}

3完善拼接回调

function genData$2(el) {    
    var data = '{';    
    if (el.events) {
        data += (genHandlers(el.events, false)) + ",";    }    data = data.replace(/,$/, '') + '}';    
    return data
}

genHandlers 遍历 el.event ,每一项的回调最外层包上一层 function 字符串,并把 所有事件 逐个拼接成 on 字符串

function genHandlers(events) {        var res = 'on:{';    
    for (var name in events) {
        res += "\"" + name + "\":" 
            + ("function($event){" 
            + (events[name].value) + ";}") + ",";
    }    
    return res.slice(0, -1) + '}'
}

转接的 初始数据和结果 像下面这样

4绑定事件

在插入 dom 之前

会调用到 updateDOMListeners,把 上面保存到 on 的 所有事件, 遍历绑定到 dom 上

updateDOMListeners 其实兜兜转转了很多方法 来处理,为了方便理解,已经非常简化,但是意思是不变的

尤大:卧槽,我写几百行,你浓缩成5行,你这是要向全国人民谢罪的啊

function updateDOMListeners(vnode) {    
    for (name in vnode.data.on) {
        vnode.elm.addEventListener(event, handler);    }}

下面所有例子使用这个vue实例,所有绑定 v-model 我都用 name

Input、Textarea

哟哟,看过 model ,就知道 这两种元素是使用 genDefaultModel 处理的

function genDefaultModel(el, value, modifiers) {    
    var code = "if($event.target.composing)return;"
        + value + '=$event.target.value;';
    addProp(el, 'value', ("(" + value + ")"));    addHandler(el, "input", code, null, true);}

绑定值

看了上面的函数,你就知道啦,input 和 textarea 调用 addProp 绑定的是 value 

拼接事件

其实这里精炼就一句话,比 jio 简单

name = $event.target.value

但是呢!input 这里其实是很复杂的,比如兼容 range 啦,预输入延迟更新啦 等等,但是现在我们不说这些,放到下篇来讲

然后,你能看到,input 和 textarea 一般绑定的是 input 事件,但是也有其他的处理,下篇讲啦

编译后的渲染 render 函数

with(this) {    
    return _c('input', {        
        directives: [{            
            name: "model",            
            rawName: "v-model",            
            value: (name),            
            expression: "name"
        }],        
        attrs: {            
            "type": "text"
        },        
        domProps: {            
            "value": (name)
        },        
        on: {            
            "input": function($event) {                
                if ($event.target.composing) return;
                name = $event.target.value;            }        }    })]}

双向更新

我们可以看到上面的 render 执行的时候,从实例读取了 name,name 收集到 本组件 watcher

1内部变化,通知更新 watcher,render 重新执行,获取新的 name,绑定到 dom 元素属性 value

2外部变化,看上面的回调事件,可以知道直接把 $event.target.value 赋值给 内部值name

Select

来看看 处理 select 的 genSelect 方法

function genSelect(el, value, modifiers) {
   
    var selectedVal = `        Array.prototype.filter.call($event.target.options,        function(o) {            return o.selected        })        .map(function(o) {            var val = \"_value\" in o ? o._value : o.value;            return  + ('val')
        })        
        ${value} = '$event.target.multiple ? $$selectedVal : $$selectedVal[0]'
    `    addHandler(el, 'change', code, null, true);}

绑定值

select 元素绑定的属性是 selectedIndex,但是 select 并没有在 genSelect 方法中调用 addProp 绑定某个属性

那么 select 在哪里设置了呢?Vue 专门使用了方法 setSelected 设置 selectedIndex,这个方法现在不说,你只要知道,他是更新 selectedIndex 的就好了,后面会有一篇专门说

疑惑为什么 select 不像 input 一样直接绑定 value,这样,不是也可以确定选项吗?

按我的理解呢,我觉得应该是原始select的 value 只有字符串一类型的值,而 Vue 的select 支持 数字和字符串两种类型的值啊

拼接事件

观察下面的渲染函数,就可以很清楚地名表,select 的回调是怎么一回事了

1从所有option 中 筛选出被选择的option

2使用数组保存所有筛选后的option的value

3判断是否多选,多选返回数组,单选返回数组第一项

然后,你还能知道 select 绑定的是 change 事件

献上 select 的渲染 render 函数

with(this) {    
    return _c('select', {        
        directives: [{            
            name: "model",            
            rawName: "v-model",            
            value: (name),            
            expression: "name"
        }],        
        on: {            
            "change": function($event) {  
             
                var $$selectedVal =
                   Array.prototype.filter
                   .call($event.target.options,function(o) {                    
                        return o.selected
                   })                   .map(function(o) {                    
                        var val = "_value" in o ? o._value: o.value;              
                        return val
                   });
                name = $event.target.multiple ? $$selectedVal: $$selectedVal[0];            }        }    })}

双向更新

render 执行时,directive 处从实例读取了 name, name 收集到 本组件 watcher

1内部变化,通知更新 watcher,上面 render 重新执行,获取新name,于是更新 select 元素属性 selectedIndex,于是select 当前选项就改变了

2外部变化,直接赋值给 绑定值,绑定值变化,通知 watcher 更新,更新完,重新设置 selectedIndex

Checkbox

genCheckboxModel 源码奉上

function genCheckboxModel(el, value, modifiers) {  
    var valueBinding = el.value || 'null';    
    var trueValueBinding = el['true-value'] || 'true';    
    var falseValueBinding = el['false-value'] || 'false';
    addProp(el, 'checked',        `Array.isArray(${value})?        _i(${value},${valueBinding})>-1        
        ${trueValueBinding === 'true'?        
        ":(" + value + ")" : ":_q(" + value + "," + trueValueBinding + ")"}`
    );
    addHandler(el, 'change',  
   
        `var $$a= ${value},             $$el=$event.target,             $$c = $$el.checked?(${trueValueBinding}):(${falseValueBinding});
        if(Array.isArray($$a)){            var $$v= (${number? '_n(' + valueBinding+")":valueBinding}),            $$i = _i($$a,$$v);            if($$el.checked){                $$i<0&&(${value}=$$a.concat([$$v]))            }else{                $$i>-1&&(${value}=$$a.slice(0,$$i).concat($$a.slice($$i+1)))            }        }else{            
            ${value} = $$c        }`,null, true
    );}

绑定值

赋值给 checked

看上面的方法就知道啦,调用 addProps,设置 checked 值

拼接事件

哈哈,还是看下面的渲染函数,看下 checkbox 的回调,其实意思就是

1数组,分是否选择

a. 选择,把当前选项 concat 进数组

b. 取消选择,把当前选项 移除出数组

2非数组,直接赋值

你还能知道 checkbox 绑定的是 change 事件

来看看checkbox 的渲染render函数

with(this) {    
    return _c('input', {        
        directives: [{            
            name: "model",            
            rawName: "v-model",            
            value: (name),            
            expression: "name"
        }],        
        attrs: {            
            "type": "checkbox",            
            "value": "1"
        },        
        domProps: {      
            // _i方法,作用是,判断第二个参数是否在 第一个参数数组中      
            "checked": Array.isArray(name) ? _i(name, "1") > -1 : (name)
        },        
        on: {            
            "change": function($event) {    
            
                var $$a = name,
                $$el = $event.target,                $$c = $$el.checked ? (true) : (false);  
              
                if (Array.isArray($$a)) {                    
                    var $$v = "1",
                    $$i = _i($$a, $$v);                    
                    if ($$el.checked) {
                        $$i < 0 && (name = $$a.concat([$$v]))                    }
                    else {
                        $$i > -1 && (name = $$a.slice(0, $$i).concat($$a.slice($$i + 1)))                    }                }
                else {
                    name = $$c                };            }        }    })}

双向更新

在 render 执行的时候,从实例读取了 name,name 收集到 本组件的 watcher

1内部变化,通知更新 watcher,上面的 render 重新执行,获取新的 name,更新 checkbox 元素属性 checked

2外部变化,判断是否多选和是否选择,来决定如何赋值 更新 绑定值 name

Radio

处理 radio 元素的 genRadioModel 源码

function genRadioModel(el, value) {    
    var valueBinding = el.value|| 'null';
    addProp(el, 'checked',
        ("_q(" + value + "," + valueBinding + ")"));
    addHandler(el, 'change',
        `${value} = ${valueBinding}`, null, true);
}

怎么赋值

直接赋值给 checked,你看上面的方法调用 addProp 可以看到

拼接事件

这个真的更加简单了...比 input 还简单啊,都不用获取值,只是直接赋值为 radio 的值

name="1", 1 是你设置给 radio 的value值

你还能知道 radio 绑定的是 change 事件

看下下面的radio 的渲染函数你就懂了

with(this) {    
    return _c('input', {        
        directives: [{            
            name: "model",            
            rawName: "v-model",            
            value: (name),            
            expression: "name"
        }],        
        attrs: {            
            "type": "radio",            
            "value": "1"
        },        
        domProps: {    
            // _q 方法,作用是,判断两个参数是否相等           
            "checked": _q(name, "1")
        },        
        on: {            
            "change": function($event) {
                name = "1";            }        }    })}

双向更新

在 render 执行的时候,绑定值 收集到 本组件的 watcher

1内部变化,通知更新 watcher,render 重新执行,获取新的 name,更新 radio 元素属性 checked

2外部变化,直接赋值 更新 绑定值 name 等于 radio元素属性 value

最后

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值