上一篇文章主要了解Vue的数据对象的构建,实际主要是attrs、props和DomProps的比较,而在template形式中有个关键点就是特殊属性的处理,而数据属性中特殊属性的处理实际上就会涉及到v-model语法糖。
本文的目标有两个:
从简单实例出发,来梳理v-model的处理逻辑:
实际上这里只需要从processAttrs中的逻辑开始即可,前面的处理实际上之前的文章都有说明,这里就不在描述了(可查看数据对象这篇文章)。
v-model虽然是Vue提供的语法糖,从类型上来说是Vue指令,对解析器来来说都是属性。所有属性的而处理都是通过processAttrs来处理的。
上图中就是processAttrs的主要处理逻辑,实际上可以清晰的看到属性的处理就是4种:
而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函数的具体逻辑。
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) + "},";
}
genDirective函数的逻辑主要如下图:
从上面逻辑可知,对于组件来说最后需要调用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属性添加到当前的节点对象上
实际上就是拿到genDirectives中新增的model属性构建出特殊形式的code,就拿上面实例来说,构建出的model对象如下:
model:{
value:(value),
callback:function ($$v) {value=$$v},
expression:"value"
}
在本次实例中使用了自定义组件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来处理相关组件的逻辑。
// 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属性中。
首先明确下特殊属性:
var acceptValue = 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之外,主要分为:
实际上从普通组件的v-model实现逻辑中可以看出可以总结出三个阶段的逻辑:
而涉及特殊属性的处理:
Vue官网中就有对表单元素的v-model的说明:
v-model
会忽略所有表单元素的value
、checked
、selected
特性的初始值而总是将 Vue 实例的数据作为数据来源。v-model` 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
- text 和 textarea 元素使用
value
属性和input
事件;- checkbox 和 radio 使用
checked
属性和change
事件;- select 字段将
value
作为 prop 并将change
作为事件。
从之前的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);
}
从之前的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=$event.target.value;
}
从之前的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绑定的是数组类型的值的特殊处理。
复选框A>
复选框B>
// 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 = 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;
}
}
从之前的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 = 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中在整个数据对象解析过程中主要的节点: