【Vue原理】VModel - 源碼版 之 表單元素綁定流程

寫文章不容易,點個讚唄兄弟 專一 Vue 源碼分享,文章分爲白話版和 源碼版,白話版助於理解工做原理,源碼版助於瞭解內部詳情,讓咱們一塊兒學習吧 研究基於 Vue版本 【2.5.17】vue

若是你以爲排版難看,請點擊 下面連接 或者 拉到 下面關注公衆號也能夠吧node

【Vue原理】VModel - 源碼版 之 表單元素綁定流程 express

今天講解 v-model 的源碼版。首先,兄弟,容我先說幾句數組

image

v-model 涉及源碼不少,篇幅很長,我都已經分了上下 三篇了,依然這麼長,可是其實內容都差很少同樣,可是我仍是毫無保留地給你了。你知道我這篇文章寫了多久,一個多星期啊,不是研究多久啊,是寫啊寫啊,不停地修修改改,一直在想如何才能講明白bash

image

若是你作好了十足的學習準備,會對你事半功倍,若是你只是看看,請看白話版吧,否則估計會越看越煩.....dom

若是你看過白話版,估計你會了解今天內容的大概,也能很快就入戲函數

今天講解不一樣表單元素的Vue是如何處理的,表單元素有學習

input、textarea、select、checkbox、radio 五大種ui

因此,咱們把每一個表單元素當作一個模塊,而後每一個模塊解決三個問題的流程,來開始咱們今天的表演this

一、v-model 如何綁定表單值

二、v-model 如何綁定事件

四、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);
    }
}
複製代碼

你也看到了,上面每種表單元素都會使用一個方法來特殊照顧,不過這些方法,做用大體同樣

一、給表單元素設置綁定值

二、給表單元素設置事件及回調

因此這裏,咱們把方法的都設計到的方法以及流程說一下

插播上面的el 是什麼?

el 是 ast,而個人理解就是解析模板後,用樹結構來表示某個dom節點,這裏先不用深究,你就只要知道他是保存解析模板後全部的數據,包括你綁定的事件,綁定的指令,綁定的屬性等等,一張圖看下

image

下面全部的處理都是以 el 爲基礎的

表單元素設置綁定值

什麼叫設置綁定值?

首先,好比你給表單元素設置 v-model ="name",name 是 內部數據吧,因此要把 name 和 表單元素 兩個牢牢綁定起來,方便後面進行雙向更新

這裏講的是每一個表單元素綁定值的流程

他們都會調用 addProp 去保存綁定的屬性 而後 綁定屬性,流程同樣,因此提出來說,可是具體綁定什麼屬性,每種元素都不盡相同,在下面表單元素模塊會詳解

一、調用 addProp,把 value 添加進 el.props

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

二、接下來的解析,el.props 會拼接成進字符串 domProps

function genData$2(el, state) {    
    var data = '{';    
    if (el.props) {

        data += "domProps:{" + (genProps(el.props)) + "},";
    }
    data = data.replace(/,$/, '') + '}';    
    return data
}
複製代碼

三、在插入 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;
        }
    }
}
複製代碼

表單元素設置事件以及回調

這裏講的是每一個表單元素綁定事件的流程

一、拼接事件

每種元素拼接事件都不同,在下面表單元素模塊會詳解

二、保存事件名和拼接好的回調

每一個元素的 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;
    }
}
複製代碼

三、完善拼接回調

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

四、綁定事件

在插入 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

一、內部變化,通知更新 watcher,render 從新執行,獲取新的 name,綁定到 dom 元素屬性 value

二、外部變化,看上面的回調事件,能夠知道直接把 $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 的回調是怎麼一回事了

一、從全部option 中 篩選出被選擇的option

二、使用數組保存全部篩選後的option的value

三、判斷是否多選,多選返回數組,單選返回數組第一項

而後,你還能知道 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

一、內部變化,通知更新 watcher,上面 render 從新執行,獲取新name,因而更新 select 元素屬性 selectedIndex,因而select 當前選項就改變了

二、外部變化,直接賦值給 綁定值,綁定值變化,通知 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 的回調,其實意思就是

一、數組,分是否選擇

a. 選擇,把當前選項 concat 進數組

b. 取消選擇,把當前選項 移除出數組

二、非數組,直接賦值

你還能知道 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

一、內部變化,通知更新 watcher,render 從新執行,獲取新的 name,更新 radio 元素屬性 checked

二、外部變化,直接賦值 更新 綁定值 name 等於 radio元素屬性 value

公衆號