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

1,125 阅读7分钟

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

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

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

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

image

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

image

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

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

今天讲解不同表单元素的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节点,这里先不用深究,你就只要知道他是保存解析模板后所有的数据,包括你绑定的事件,绑定的指令,绑定的属性等等,一张图看下

image

下面所有的处理都是以 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 是拼接的回调

image

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) + '}'
}

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

image

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

image


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 函数

image

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 函数

image

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函数

image

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 的渲染函数你就懂了

image

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

公众号