乐趣区

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

写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue 版本 【2.5.17】

如果你觉得排版难看,请点击 下面链接 或者 拉到 下面 关注公众号 也可以吧

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

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

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

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

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

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

input、textarea、select、checkbox、radio 五大种

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

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};
            }
        }
    })
}

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

退出移动版