Vue之v-model

前言
上一篇文章主要了解Vue的数据对象的构建,实际主要是attrs、props和DomProps的比较,而在template形式中有个关键点就是特殊属性的处理,而数据属性中特殊属性的处理实际上就会涉及到v-model语法糖。
【Vue之v-model】本文的目标有两个:

  • v-model语法糖的实现逻辑
  • 涉及特殊属性的v-model的特殊处理
v-model实现逻辑
从简单实例出发,来梳理v-model的处理逻辑:

实际上这里只需要从processAttrs中的逻辑开始即可,前面的处理实际上之前的文章都有说明,这里就不在描述了(可查看数据对象这篇文章)。
解析阶段 v-model虽然是Vue提供的语法糖,从类型上来说是Vue指令,对解析器来来说都是属性。所有属性的而处理都是通过processAttrs来处理的。
Vue之v-model
文章图片

上图中就是processAttrs的主要处理逻辑,实际上可以清晰的看到属性的处理就是4种:
  • 普通属性
  • prop
  • v-on事件绑定
  • 指令
而v-model就是指令,Vue所有指令都是以v-开头的,所以就具体看下指令的处理逻辑,代码量不多具体如下:
// v-model -> model,去除v-前缀 name = name.replace(dirRE, ''); // /:(.*)$/,例如v-model:test var argMatch = name.match(argRE); var arg = argMatch && argMatch[1]; if (arg) { name = name.slice(0, -(arg.length + 1)); } // 添加到directives对象中 addDirective(el, name, rawName, value, arg, modifiers); if ("development" !== 'production' && name === 'model') { checkForAliasModel(el, value); }

通过processAttrs会将v-model保存到directives对象中,下一步需要专注的逻辑点就是依据数据对象来创建render函数,实际上就是generate函数的具体逻辑。
构建Render阶段
generate -> genElement -> genData$2
generate中的主要处理逻辑就是genElement函数,而v-model属于数据对象中指令调用genData$2来处理,其中针对v-model有特殊的处理逻辑:
var dirs = genDirectives(el, state); if (dirs) { data += dirs + ','; } // component v-model if (el.model) { data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},"; }

genDirectives genDirective函数的逻辑主要如下图:
Vue之v-model
文章图片

从上面逻辑可知,对于组件来说最后需要调用genComponentModel函数来处理相关逻辑。
function genComponentModel ( el, value, modifiers ) { var ref = modifiers || {}; // .number修饰符、.trim修饰符 var number = ref.number; var trim = ref.trim; var baseValueExpression = '$$v'; var valueExpression = baseValueExpression; if (trim) { valueExpression = "(typeof " + baseValueExpression + " === 'string'" + "? " + baseValueExpression + ".trim()" + ": " + baseValueExpression + ")"; } if (number) { valueExpression = "_n(" + valueExpression + ")"; } var assignment = genAssignmentCode(value, valueExpression); // 增加model属性 el.model = { value: ("(" + value + ")"), expression: ("\"" + value + "\""), callback: ("function (" + baseValueExpression + ") {" + assignment + "}") }; }

genDirectives实际上就是区分组件和表单元素等v-model的处理,同时定义model属性添加到当前的节点对象上
model属性的处理 实际上就是拿到genDirectives中新增的model属性构建出特殊形式的code,就拿上面实例来说,构建出的model对象如下:
model:{ value:(value), callback:function ($$v) {value=https://www.it610.com/article/$$v}, expression:"value" }

render函数执行阶段 在本次实例中使用了自定义组件el-input,无论是自定义组件还是HTML标签都会调用_c实例方法即底层调用createElement,Vue提供的创建节点的方法(手动构建render函数也是需要显式调用该方法)。
createElement函数内部调用_createElement函数,这边核心的逻辑就是创建VNode,主要代码如下:
if (typeof tag === 'string') { var Ctor; // html标签或svg标签 if (config.isReservedTag(tag)) { vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ); } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // components中存在的组件 vnode = createComponent(Ctor, data, context, children, tag); } else { vnode = new VNode( tag, data, children, undefined, undefined, context ); } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children); }

应为el-input是全局注册的组件,所以:
Ctor = resolveAsset(context.$options, 'components', tag)

这里会返回el-input的构造函数,所以必然会调用createComponent来处理相关组件的逻辑。
createComponent函数
// transform component v-model data into props & events if (isDef(data.model)) { transformModel(Ctor.options, data); }

function transformModel (options, data) { // 默认prop是value var prop = (options.model && options.model.prop) || 'value'; // 默认event是Input var event = (options.model && options.model.event) || 'input'; // 保存到props属性中 (data.props || (data.props = {}))[prop] = data.model.value; // 将input事件添加到on对象中 var on = data.on || (data.on = {}); if (isDef(on[event])) { on[event] = [data.model.callback].concat(on[event]); } else { on[event] = data.model.callback; } }

从这里可以看出,model的定义最后转换成实际上就是:props对象value + on对象input事件,在VNode对象中会保存到data属性和componentOptions属性中。
涉及特殊属性的v-model的特殊处理
首先明确下特殊属性:
var acceptValue = https://www.it610.com/article/makeMap('input,textarea,option,select,progress'); var mustUseProp = function (tag, type, attr) { return ( (attr === 'value' && acceptValue(tag)) && type !== 'button' || (attr === 'selected' && tag === 'option') || (attr === 'checked' && tag === 'input') || (attr === 'muted' && tag === 'video') ) };

本次v-model实际实际上关注的是处了video之外,主要分为:
  • input[type=“radio”]
  • input[type=“checkbox”]
  • input[attr=“value”]
实际上从普通组件的v-model实现逻辑中可以看出可以总结出三个阶段的逻辑:
  • parse阶段中的processAttrs
  • render函数构建阶段genData$2
  • render函数执行_createElement中正对标签和组件的处理
而涉及特殊属性的处理:
  • parse阶段就不在关注了,即特殊属性都放在props对象中了
  • render函数构建阶段-genDirectives
  • render执行阶段-_createElement
Vue官网中就有对表单元素的v-model的说明:
v-model 会忽略所有表单元素的 valuecheckedselected 特性的初始值而总是将 Vue 实例的数据作为数据来源。
v-model` 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
  • text 和 textarea 元素使用 value 属性和 input 事件;
  • checkbox 和 radio 使用 checked 属性和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。
input radio 从之前的genDirectives中逻辑图中可知:
if (tag === 'input' && type === 'radio') { genRadioModel(el, value, modifiers); }

function genRadioModel ( el, value, modifiers ) { // .number var number = modifiers && modifiers.number; // value属性 var valueBinding = getBindingAttr(el, 'value') || 'null'; valueBinding = number ? ("_n(" + valueBinding + ")") : valueBinding; // 添加checked属性到props, _q对应的实例方法是looseEqual,比较两个数是否相等 addProp(el, 'checked', ("_q(" + value + "," + valueBinding + ")")); // change事件 addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true); }

input或textarea value 从之前的genDirectives中逻辑图中可知 input和textarea会调用genDefaultModel函数,而该函数的处理逻辑就相对radio复杂些。
function genDefaultModel ( el, value, modifiers ) { var type = el.attrsMap.type; // 判断是否value作为prop,会与v-model冲突报警告 { 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' ); } }var ref = modifiers || {}; // .lazy修饰符 var lazy = ref.lazy; // .number修饰符 var number = ref.number; // .trim修饰符 var trim = ref.trim; var needCompositionGuard = !lazy && type !== 'range'; // lazy就使用change事件否则使用input事件,range的兼容处理:IE只支持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 + ")"; }var code = genAssignmentCode(value, valueExpression); if (needCompositionGuard) { // composing判断是否在输入中,防止输入中响应式属性更新 code = "if($event.target.composing)return; " + code; } // value属性添加到props中 addProp(el, 'value', ("(" + value + ")")); // 绑定事件 addHandler(el, event, code, null, true); if (trim || number) { // blur事件,.trim、.number修饰符会强制更新 addHandler(el, 'blur', '$forceUpdate()'); } }

实际上input或textarea的v-model的callback:
callback: function($event) { if ($event.target.composing)return; value=https://www.it610.com/article/$event.target.value; }

input checkbox 从之前的genDirectives中逻辑图中可知 input和textarea会调用genCheckboxModel函数,具体逻辑如下:
function genCheckboxModel ( el, value, modifiers ) { var number = modifiers && modifiers.number; // value属性 var valueBinding = getBindingAttr(el, 'value') || 'null'; // 支持true-value和false-value属性,自定义属性非HTML标准特性 var trueValueBinding = getBindingAttr(el, 'true-value') || 'true'; var falseValueBinding = getBindingAttr(el, 'false-value') || 'false'; // checked属性添加到props对象中 addProp(el, 'checked', "Array.isArray(" + value + ")" + "?_i(" + value + "," + valueBinding + ")>-1" + ( trueValueBinding === 'true' ? (":(" + value + ")") : (":_q(" + value + "," + trueValueBinding + ")") ) ); // v-model-change事件 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&&(" + (genAssignmentCode(value, '$$a.concat([$$v])')) + ")}" + "else{$$i>-1&&(" + (genAssignmentCode(value, '$$a.slice(0,$$i).concat($$a.slice($$i+1))')) + ")}" + "}else{" + (genAssignmentCode(value, '$$c')) + "}", null, true ); }

这里需要注意的是checkbox如果v-model绑定的是数组类型的值的特殊处理。

// checked属性的计算逻辑 const checked = Array.isArray(value) // 判断checkList是否存在当前chekcbox的value属性值 ? _i(checkList, valueBinding})>-1 : trueValueBinding === true ? checkList : _q(checkList, trueValueBinding)// change事件的函数体 const change = function($event) { let checkList = checkList; const target = $event.target; if (Array.isArray(checkList)) { const formatValue = https://www.it610.com/article/number ? _n(valueBinding) : valueBinding; // 判断checkList是否存在当前chekcbox的value属性值,获取其下标 const index = _i(checkList, formatValue); if (target.checked) { // 不存在的但是选中状态 index < 0 && (checkList = checkList.concat([formatValue])); } else { index> -1 && ( checkList = checkList .slice(0,index) .concat(checkList.slice(index+1)) ); } } else { checkList = target.checked ? trueValueBinding : falseValueBinding; } }

Select标签 从之前的genDirectives中逻辑图中可知select会调用genSelect函数,具体逻辑如下:
function genSelect ( el, value, modifiers ) { // number修饰符 var number = modifiers && modifiers.number; 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 " + (number ? '_n(val)' : 'val') + "})"; var assignment = '$event.target.multiple ? $$selectedVal : $$selectedVal[0]'; var code = "var $$selectedVal = " + selectedVal + "; "; code = code + " " + (genAssignmentCode(value, assignment)); addHandler(el, 'change', code, null, true); }

const change = function($event) { const { options, multiple } = $event.target; const selectedVal = Array.prototype.filter.call(options, function(o) { return o.selected; }) .map(function(o) { const val = "_value" in o ? o._value : o.value; return number ? _n(val) : val; }); // value就是v-model绑定的值 value = https://www.it610.com/article/multiple ? selectedVal : selectedVal[0] }

总结
v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件:
  • text 和 textarea 元素使用 value属性和 input 事件
    即value属性存放在props,不可在显式存在:value属性,否则会报warning
  • checkbox 和 radio 使用 checked 属性和 change事件
    即checked属性存放在props中,支持数组类型值(value属性表示当前checkbox或radio的值)
  • select 字段将 value 作为 prop 并将 change作为事件
    即value会存放在props中
实际上Vue对于表单元素的特殊处理,也是源于DOM操作表单元素的基础来的,比如DOM操作checkbox就是通过设置checked来实现选中和未选中的。
唯一不同的是Vue双向绑定,v-model会忽略所有表单元素的 value、checked、selected特性的初始值而总是将 Vue 实例的数据作为数据来源。
这里总结下Vue中在整个数据对象解析过程中主要的节点:
  • pasre阶段(即调用parseHTML函数,只有通过template形式才会存在)的handleSatrtTag的处理逻辑,涉及到slot、v-for、v-if等所有作为HTML特性的解析处理,其中processAttrs就是普通属性和v-on、v-bind、:的处理逻辑
  • render函数构建阶段(即调用generate函数,只有通过template形式才会存在)的genElement的主要处理逻辑,这里涉及到所有的数据对象的形式构建,用于组成render函数
  • render函数执行阶段,即主要是$createElement中的主要处理来生成VNode

    推荐阅读