好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

vue动态绑定v-model属性名方式

vue动态绑定v-model属性名

1.目标

首先配置列,根据配置渲染表单,每个表单项绑定配置中的属性

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响应式原理,响应式主要的效果是数据改变了就会引起页面修改。关于v-model我们也不陌生,vue的双向绑定指令,页面修改会引起数据修改,数据修改页面也会跟着改变。我们直到数据->页面是由vue的响应式原理实现的,那么该怎么做到页面->数据的修改呢?

表单绑定

v-model一般我们是在表单元素上进行使用,因为视图能影响数据,本质上是这个视图需要可交互,因此表单是实现这一交互的前提。表单的使用是以<input>、<textarea>、<select>为核心。具体的使用细节这里就不一一细说了。

这里我们从模板解析开始分析,vue对v-model做了什么操作。

这里我们来看一些绑定在input上的v-model都经历了什么。

// 普通输入框
<input type="text" v-model="value1">

AST树的解析

模版的编译阶段,会调用var ast = parse(template.trim(), options)生成AST树,parse函数的起他细节这里不展开分析,我们只说模板属性上的解析processAttrs函数。

vue模板属性有两部分组成,一部分是指令,另一部分是普通的html标签属性。对于指令,出去v-on和v-bind,其他普通指令会执行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产生阶段对事件指令v-on的处理是为AST树添加events属性。类似的,普通指令会在AST树上添加directives属性,我们可以看一下addDirective函数

// 添加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函数生成

render函数生成阶段,generate逻辑,其中genData会对模版的各个属性进行处理,最终返回拼接好的字符串模板,而对指令的处理会进入genDirectives函数

在genDirectives函数中,会拿到之前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回去执行对应的model函数。我们来看一下model函数的逻辑。

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的处理,在这一步上会根据使用场景处理调用不同的处理。单是对每种类型对应的事件处理响应机制也不同。因此我们需要针对不同的表单控件生成不同的render函数,所以需要产生不同的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修饰符将触发同步的事件从input改为change
? ? var event = lazy ? 'change' : type === 'range' ? RANGE_TOKEN : 'input';
? ? var valueExpression = '$event.target.value';
? ? // 过滤用户输入的首尾空白符
? ? if (trim) {
? ? ? valueExpression = "$event.target.value.trim()";
? ? }
? ? // 将用户输入转为数值类型
? ? if (number) {
? ? ? valueExpression = "_n(" + valueExpression + ")";
? ? }
? ? // genAssignmentCode函数是为了处理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 + ")")
? }
}

该函数主要逻辑是两个部分,一部分是针对修饰符产生不同的事件处理字符串,二是为v-model产生的AST树,添加属性和事件相关的属性。其中最核心的两行代码是:

// ?添加value属性
addProp(el, 'value', ("(" + value + ")"));
// 绑定事件属性
addHandler(el, event, code, null, true);

addHandler函数会为AST树添加事件相关的属性,addProp会为AST树添加props属性。最终AST树新增了两个属性。

到这里我们会发现,通过genDirective处理后,原先的AST树新增了两个属性。所以在字符串生成阶段同样需要处理props和event的分支

function genData$2 (el, state) {
? var data = '{';
? // 已经分析过的genDirectives
? var dirs = genDirectives(el, state);
? // 处理props
? if (el.props) {
? ? data += "domProps:" + (genProps(el.props)) + ",";
? }
? // 处理事件
? if (el.events) {
? ? data += (genHandlers(el.events, false)) + ",";
? }
}

最终render函数的结果为:

"_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,在该函数中判断该属性是否为指令,是指令判断是不是v-on、v-bind,如果都不是就进入addDirective 通过addDirective函数为AST树上添加了directives中的一个对象 然后根据AST树,生成render函数过程中,需要在genData中调用genDirectives,进入指令处理流程。 genDirectives拿到指令对象后,会遍历指令对象,使用state.directives[dir.name]对指令对象进行解析和处理。这里的dir.name是model。会调用一个model函数,在该函数中根据v-model的应用标签类型,处理成不同的AST属性。input输入框类型,会调用genDefaultModel函数,在其中做类型判断,修饰符处理,然后通过addProp为AST添加props属性,addHandler会为AST语法树的events属性中添加对应事件监听。 最后根据AST语法树生成render函数时,绑定的属性会以props的形式存在domProps中,另一个是以事件的形式存储input事件,并保留在on属性中

patch真实节点

当我们的render函数生成以后,执行render函数,生成对应的vnode。 

有了新的vnode以后需要执行patchVnode。前面得到的指令相关的信息会保留在vnode的data属性里,所以对属性的处理也会走针对指令处理的函数incokeCreateHooks。

在该函数中对指令的处理包括:

判断vnode data上存在domProps属性,调用 updateDOMProps更新input标签的value值 调用updateAttrs函数,根据attrs属性更新节点的属性值 判断vnode data上存在on属性,调用updateDomListeners为dom添加事件监听

总结:所以v-model语法糖最终是通过监听表单控件自身的某些事件(不同类型的标签会有不同的监听事件类型,后面会列出来),去影响自身的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本质上还是一个子父组件通信的语法糖。 

以上为个人经验,希望能给大家一个参考,也希望大家多多支持。 

查看更多关于vue动态绑定v-model属性名方式的详细内容...

  阅读:66次