vue動態(tài)綁定v-model屬性名方式
vue動態(tài)綁定v-model屬性名
1.目標
首先配置列,根據(jù)配置渲染表單,每個表單項綁定配置中的屬性
2.方案
<template v-for="(item) in showQueryColumns" > <el-col :key="item.prop" :xs = "24" :sm = "12" :md="12" :lg = "12" :xl = "6"> <!--字符串類型--> <el-form-item v-if="item.type==='string'" :label="$t(item.i18n)" :prop="item.prop"> <el-input v-model="form[item.prop]" clearable></el-input> </el-form-item> </el-col> </template>
- v-model綁定的必須是屬性,可以使用方括號,取指定對象的屬性
親測有效
vue雙向綁定原理(v-model)
之前有整理過Vue響應式原理,響應式主要的效果是數(shù)據(jù)改變了就會引起頁面修改。關于v-model我們也不陌生,vue的雙向綁定指令,頁面修改會引起數(shù)據(jù)修改,數(shù)據(jù)修改頁面也會跟著改變。我們直到數(shù)據(jù)->頁面是由vue的響應式原理實現(xiàn)的,那么該怎么做到頁面->數(shù)據(jù)的修改呢?
表單綁定
v-model一般我們是在表單元素上進行使用,因為視圖能影響數(shù)據(jù),本質上是這個視圖需要可交互,因此表單是實現(xiàn)這一交互的前提。表單的使用是以<input>、<textarea>、<select>為核心。具體的使用細節(jié)這里就不一一細說了。
這里我們從模板解析開始分析,vue對v-model做了什么操作。
這里我們來看一些綁定在input上的v-model都經(jīng)歷了什么。
// 普通輸入框 <input type="text" v-model="value1">
AST樹的解析
模版的編譯階段,會調用var ast = parse(template.trim(), options)生成AST樹,parse函數(shù)的起他細節(jié)這里不展開分析,我們只說模板屬性上的解析processAttrs函數(shù)。
vue模板屬性有兩部分組成,一部分是指令,另一部分是普通的html標簽屬性。對于指令,出去v-on和v-bind,其他普通指令會執(zhí)行addDirective過程。
// 處理模板屬性 function processAttrs(el) { ? var list = el.attrsList; ? var i, l, name, rawName, value, modifiers, syncGen, isDynamic; ? for (i = 0, l = list.length; i < l; i++) { ? ? name = rawName = list[i].name; // v-on:click ? ? value = list[i].value; // doThis ? ? if (dirRE.test(name)) { // 1.針對指令的屬性處理 ? ? ? ··· ? ? ? if (bindRE.test(name)) { // v-bind分支 ? ? ? ? ··· ? ? ? } else if(onRE.test(name)) { // v-on分支 ? ? ? ? ··· ? ? ? } else { // 除了v-bind,v-on之外的普通指令 ? ? ? ? ··· ? ? ? ? // 普通指令會在AST樹上添加directives屬性 ? ? ? ? addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i]); ? ? ? ? if (name === 'model') { ? ? ? ? ? checkForAliasModel(el, value); ? ? ? ? } ? ? ? } ? ? } else { ? ? ? // 2. 普通html標簽屬性 ? ? } ? } }
在AST產(chǎn)生階段對事件指令v-on的處理是為AST樹添加events屬性。類似的,普通指令會在AST樹上添加directives屬性,我們可以看一下addDirective函數(shù)
// 添加directives屬性 function addDirective (el,name,rawName,value,arg,isDynamicArg,modifiers,range) { ? ? (el.directives || (el.directives = [])).push(rangeSetItem({ ? ? ? name: name, ? ? ? rawName: rawName, ? ? ? value: value, ? ? ? arg: arg, ? ? ? isDynamicArg: isDynamicArg, ? ? ? modifiers: modifiers?? ?// 模板中添加的修飾符,如:.lazy、.number、.trim ? ? }, range)); ? ? el.plain = false; ? }
最終AST樹上會多處一個屬性對象
// AST { ? directives: { ? ? { ? ? ? rawName: 'v-model', ? ? ? value: 'value', ? ? ? name: 'v-model', ? ? ? modifiers: undefined ? ? } ? } }
render函數(shù)生成
render函數(shù)生成階段,generate邏輯,其中genData會對模版的各個屬性進行處理,最終返回拼接好的字符串模板,而對指令的處理會進入genDirectives函數(shù)
在genDirectives函數(shù)中,會拿到之前AST樹中的directives對象,并遍歷解析指令對象,最終以'directives:['包裹的字符串返回。
// directives render字符串的生成 ? function genDirectives (el, state) { ? ? // 拿到指令對象 ? ? var dirs = el.directives; ? ? if (!dirs) { return } ? ? // 字符串拼接 ? ? var res = 'directives:['; ? ? var hasRuntime = false; ? ? var i, l, dir, needRuntime; ? ? for (i = 0, l = dirs.length; i < l; i++) { ? ? ? dir = dirs[i]; ? ? ? needRuntime = true; ? ? ? // 對指令ast樹的重新處理 ? ? ? var gen = state.directives[dir.name]; ? ? ? if (gen) { ? ? ? ? // compile-time directive that manipulates AST. ? ? ? ? // returns true if it also needs a runtime counterpart. ? ? ? ? needRuntime = !!gen(el, dir, state.warn); ? ? ? } ? ? ? if (needRuntime) { ? ? ? ? hasRuntime = true; ? ? ? ? res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:" + (dir.isDynamicArg ? dir.arg : ("\"" + (dir.arg) + "\""))) : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},"; ? ? ? } ? ? } ? ? if (hasRuntime) { ? ? ? return res.slice(0, -1) + ']' ? ? } ? }
這里有一句關鍵代碼var gen = state.directives[dir.name],這里的的dir.name為model,這個model回去執(zhí)行對應的model函數(shù)。我們來看一下model函數(shù)的邏輯。
unction model (el,dir,_warn) { ? ? warn$1 = _warn; ? ? // 綁定的值 ? ? var value = dir.value; ? ? var modifiers = dir.modifiers; ? ? var tag = el.tag; ? ? var type = el.attrsMap.type; ? ? { ? ? ? // 這里遇到type是file的html,如果還使用雙向綁定會報出警告。 ? ? ? // 因為File inputs是只讀的 ? ? ? if (tag === 'input' && type === 'file') { ? ? ? ? warn$1( ? ? ? ? ? "<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" + ? ? ? ? ? "File inputs are read only. Use a v-on:change listener instead.", ? ? ? ? ? el.rawAttrsMap['v-model'] ? ? ? ? ); ? ? ? } ? ? } ? ? //組件上v-model的處理 ? ? if (el.component) { ? ? ? genComponentModel(el, value, modifiers); ? ? ? // component v-model doesn't need extra runtime ? ? ? return false ? ? } else if (tag === 'select') { ? ? ? // select表單 ? ? ? genSelect(el, value, modifiers); ? ? } else if (tag === 'input' && type === 'checkbox') { ? ? ? // checkbox表單 ? ? ? genCheckboxModel(el, value, modifiers); ? ? } else if (tag === 'input' && type === 'radio') { ? ? ? // radio表單 ? ? ? genRadioModel(el, value, modifiers); ? ? } else if (tag === 'input' || tag === 'textarea') { ? ? ? // 普通input,如 text, textarea ? ? ? genDefaultModel(el, value, modifiers); ? ? } else if (!config.isReservedTag(tag)) { ? ? ? genComponentModel(el, value, modifiers); ? ? ? // component v-model doesn't need extra runtime ? ? ? return false ? ? } else { ? ? ? // 如果不是表單使用v-model,同樣會報出警告,雙向綁定只針對表單控件。 ? ? ? warn$1( ? ? ? ? "<" + (el.tag) + " v-model=\"" + value + "\">: " + ? ? ? ? "v-model is not supported on this element type. " + ? ? ? ? 'If you are working with contenteditable, it\'s recommended to ' + ? ? ? ? 'wrap a library dedicated for that purpose inside a custom component.', ? ? ? ? el.rawAttrsMap['v-model'] ? ? ? ); ? ? } ? ? // ensure runtime directive metadata ? ? //? ? ? return true ? }
我們可以看到對于v-model的處理,在這一步上會根據(jù)使用場景處理調用不同的處理。單是對每種類型對應的事件處理響應機制也不同。因此我們需要針對不同的表單控件生成不同的render函數(shù),所以需要產(chǎn)生不同的AST屬性。model針對不同類型的表單控件有不同的處理分支。我們來看普通input標簽的處理,genDefalutModel分支。
function genDefaultModel (el,value,modifiers) { ? ? var type = el.attrsMap.type; ? ? // v-model和v-bind值相同值,有沖突會報錯 ? ? { ? ? ? var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value']; ? ? ? var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']; ? ? ? if (value$1 && !typeBinding) { ? ? ? ? var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'; ? ? ? ? warn$1( ? ? ? ? ? binding + "=\"" + value$1 + "\" conflicts with v-model on the same element " + ? ? ? ? ? 'because the latter already expands to a value binding internally', ? ? ? ? ? el.rawAttrsMap[binding] ? ? ? ? ); ? ? ? } ? ? } ? ? // modifiers存貯的是v-model的修飾符。 ? ? var ref = modifiers || {}; ? ? // lazy,trim,number是可供v-model使用的修飾符 ? ? var lazy = ref.lazy; ? ? var number = ref.number; ? ? var trim = ref.trim; ? ? var needCompositionGuard = !lazy && type !== 'range'; ? ? // lazy修飾符將觸發(fā)同步的事件從input改為change ? ? var event = lazy ? 'change' : type === 'range' ? RANGE_TOKEN : 'input'; ? ? var valueExpression = '$event.target.value'; ? ? // 過濾用戶輸入的首尾空白符 ? ? if (trim) { ? ? ? valueExpression = "$event.target.value.trim()"; ? ? } ? ? // 將用戶輸入轉為數(shù)值類型 ? ? if (number) { ? ? ? valueExpression = "_n(" + valueExpression + ")"; ? ? } ? ? // genAssignmentCode函數(shù)是為了處理v-model的格式,允許使用以下的形式: v-model="a.b" v-model="a[b]" ? ? var code = genAssignmentCode(value, valueExpression); ? ? if (needCompositionGuard) { ? ? ? // ?保證了不會在輸入法組合文字過程中得到更新 ? ? ? code = "if($event.target.composing)return;" + code; ? ? } ? ? // ?添加value屬性 ? ? addProp(el, 'value', ("(" + value + ")")); ? ? // 綁定事件 ? ? addHandler(el, event, code, null, true); ? ? if (trim || number) { ? ? ? addHandler(el, 'blur', '$forceUpdate()'); ? ? } ? } function genAssignmentCode (value,assignment) { ? // 處理v-model的格式,v-model="a.b" v-model="a[b]" ? var res = parseModel(value); ? if (res.key === null) { ? ? // 普通情形 ? ? return (value + "=" + assignment) ? } else { ? ? // 對象形式 ? ? return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")") ? } }
該函數(shù)主要邏輯是兩個部分,一部分是針對修飾符產(chǎn)生不同的事件處理字符串,二是為v-model產(chǎn)生的AST樹,添加屬性和事件相關的屬性。其中最核心的兩行代碼是:
// ?添加value屬性 addProp(el, 'value', ("(" + value + ")")); // 綁定事件屬性 addHandler(el, event, code, null, true);
addHandler函數(shù)會為AST樹添加事件相關的屬性,addProp會為AST樹添加props屬性。最終AST樹新增了兩個屬性。
到這里我們會發(fā)現(xiàn),通過genDirective處理后,原先的AST樹新增了兩個屬性。所以在字符串生成階段同樣需要處理props和event的分支
function genData$2 (el, state) { ? var data = '{'; ? // 已經(jīng)分析過的genDirectives ? var dirs = genDirectives(el, state); ? // 處理props ? if (el.props) { ? ? data += "domProps:" + (genProps(el.props)) + ","; ? } ? // 處理事件 ? if (el.events) { ? ? data += (genHandlers(el.events, false)) + ","; ? } }
最終render函數(shù)的結果為:
"_c('input', { ? directives:[{ ? ? ?name:"model", ? ? ?rawName:"v-model", ? ? ?value:(message), ? ? ?expression:"message" ? ?}], ? ?attrs:{"type":"text"}, ? ?domProps:{"value":(message)}, ? ?on:{ ? ? ?"input":function($event){ ? ? ? ?if($event.target.composing) ? ? ? ? ?return;message=$event.target.value ? ? ?} ? ?} })"
總結
如果到這里比較迷糊的話,我們來整理一下整體的流程
- 在生成AST階段,處理到屬性時進入processAttrs,在該函數(shù)中判斷該屬性是否為指令,是指令判斷是不是v-on、v-bind,如果都不是就進入addDirective
- 通過addDirective函數(shù)為AST樹上添加了directives中的一個對象
- 然后根據(jù)AST樹,生成render函數(shù)過程中,需要在genData中調用genDirectives,進入指令處理流程。
- genDirectives拿到指令對象后,會遍歷指令對象,使用state.directives[dir.name]對指令對象進行解析和處理。這里的dir.name是model。會調用一個model函數(shù),在該函數(shù)中根據(jù)v-model的應用標簽類型,處理成不同的AST屬性。input輸入框類型,會調用genDefaultModel函數(shù),在其中做類型判斷,修飾符處理,然后通過addProp為AST添加props屬性,addHandler會為AST語法樹的events屬性中添加對應事件監(jiān)聽。
- 最后根據(jù)AST語法樹生成render函數(shù)時,綁定的屬性會以props的形式存在domProps中,另一個是以事件的形式存儲input事件,并保留在on屬性中
patch真實節(jié)點
當我們的render函數(shù)生成以后,執(zhí)行render函數(shù),生成對應的vnode。
有了新的vnode以后需要執(zhí)行patchVnode。前面得到的指令相關的信息會保留在vnode的data屬性里,所以對屬性的處理也會走針對指令處理的函數(shù)incokeCreateHooks。
在該函數(shù)中對指令的處理包括:
- 判斷vnode data上存在domProps屬性,調用 updateDOMProps更新input標簽的value值
- 調用updateAttrs函數(shù),根據(jù)attrs屬性更新節(jié)點的屬性值
- 判斷vnode data上存在on屬性,調用updateDomListeners為dom添加事件監(jiān)聽
總結:所以v-model語法糖最終是通過監(jiān)聽表單控件自身的某些事件(不同類型的標簽會有不同的監(jiān)聽事件類型,后面會列出來),去影響自身的value值。等同于
<input type="text" :value="message" @input="(e) => {this.message = e.target.value}">
不同的表單控件綁定的事件總結
上面我們講到v-model其實是一個語法糖,他本質包含了兩個操作:
v-bind
綁定了一個value屬性v-on
指令給當前元素綁定對應事件,默認是input事件
原理說過了我們這里來整理一下,表單綁定v-model不同的控件到底是綁定了什么事件。
change事件
select
checkbox
radio
input事件
這是默認時間,當不是上面三種表單元素時,會解析成input事件,比如text、number等input元素和textarea
組件使用v-model
由上面的原理我們可以看出來組件上使用v-model也是類似的流程,本質上是子父組件通信的語法糖。看一個使用案例:
var child = { ? ? template: '<div><input type="text" :value="value" @input="emitEvent">{{value}}</div>', ? ? methods: { ? ? ? emitEvent(e) { ? ? ? ? this.$emit('input', e.target.value) ? ? ? } ? ? }, ? ? props: ['value'] ? } ?new Vue({ ? ?data() { ? ? ?return { ? ? ? ?message: 'test' ? ? ?} ? ?}, ? ?components: { ? ? ?child ? ?}, ? ?template: '<div id="app"><child v-model="message"></child></div>', ? ?el: '#app' ?})
父組件上使用v-model,子組件默認會利用名為value的prop和名為input的事件。
源碼這里就不詳細說了,子組件的vnode會為data.props添加data.model.value,并且給data.on添加data.model.callback。因此父組件的語法糖本質上可以修改為
<child :value = "message" @input="function(e){message = e}"</child>
顯然,這種寫法就是事件通信的寫法,這個過程又回到對事件指令的分析過程了。因此我們可以很明顯的意識到,組件使用v-model本質上還是一個子父組件通信的語法糖。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。