Vue之v-model
前言上一篇文章主要了解Vue的数据对象的构建,实际主要是attrs、props和DomProps的比较,而在template形式中有个关键点就是特殊属性的处理,而数据属性中特殊属性的处理实际上就会涉及到v-model语法糖。本文的目标有两个:v-model语法糖的实现逻辑涉及特殊属性的v-model的特殊处理v-model实现逻辑从简单实例出发,来梳理v-model的处理逻辑:&...
前言
上一篇文章主要了解Vue的数据对象的构建,实际主要是attrs、props和DomProps的比较,而在template形式中有个关键点就是特殊属性的处理,而数据属性中特殊属性的处理实际上就会涉及到v-model语法糖。
本文的目标有两个:
- v-model语法糖的实现逻辑
- 涉及特殊属性的v-model的特殊处理
v-model实现逻辑
从简单实例出发,来梳理v-model的处理逻辑:
<div id="app">
<my-component v-model="text" />
</div>
<script>
export default {
data() {
return {
text: '测试'
}
}
}
</script>
实际上这里只需要从processAttrs中的逻辑开始即可,前面的处理实际上之前的文章都有说明,这里就不在描述了(可查看数据对象这篇文章)。
解析阶段
v-model虽然是Vue提供的语法糖,从类型上来说是Vue指令,对解析器来来说都是属性。所有属性的而处理都是通过processAttrs来处理的。
上图中就是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对象中,在directives中保存的model的数据结构如下:
{
arg: null
modifiers: undefined
name: "model"
rawName: "v-model"
value: "text"
}
下一步需要专注的逻辑点就是依据数据对象来创建render函数,实际上就是generate函数的具体逻辑。
构建Render函数阶段
当对模板解析完成后即生成AST之后,就调用generate函数做一些编译处理,输出最终的code。
function generate (
ast,
options
) {
var state = new CodegenState(options);
// ast存在就会调用genElement输出最终的code
var code = ast ? genElement(ast, state) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}
generate中的主要处理逻辑就是genElement函数,genElement函数中会区分组件和原生标签的处理逻辑,简单来说就是:
- 对于组件,会调用genComponent函数来生成对应code
- 对于原生标签,直接调用genData$2来生成对应code、调用genChildren来生成对应子内容code
实际上genComponen函数内部逻辑也是调用genData$2、genChildren来生成对应的code和children code,但是顺序不同。
对于组件,是先生成子内容的code,再生成自身code即先调用genChildren、再调用genData$2
所有逻辑都指向genData$2,该函数的处理逻辑是用来处理指令、key、ref、attrs、props等VNode的数据对象。v-model是Vue中的指令,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) + "},";
}
上面的逻辑实际上分为两部分:
- 首先是指令的处理,因为v-model也是指令所以会存在这部分的逻辑
- model code的构建,这里比较明了
genDirectives
genDirective函数的逻辑主要如下图:
从上面逻辑可知,对于组件来说最后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 + "}")
};
}
从上面逻辑中可以提取出几个关键的逻辑:
- 指令的trim、number修饰符的支持逻辑
- genAssignmentCode生成对应的code
- model对象的构建:value、callback、expression
这里结合上面实例来说,构建出的model对象如下:
model:{
value:(value),
callback:function ($$v) {value=$$v},
expression:"value"
}
当在组件上使用v-model指令,该指令会归属于应用的组件,子组件内部触发相关事件,v-model是如何实现双向绑定的呢?
从上面的结构可知,必然是调用callback函数,来实现双向绑定效果的
Vue官方对于v-model的简单概括就是:props属性 + input/change事件。从上面知道内部触发input/change事件就会执行相关事件的回调函数,这里涉及到组件事件的处理。
对于组件v-model指令,在render生成阶段是没有注册相关事件,这里先说明下Vue中关于子组件的创建处理实际上分为两个阶段:
- 显式调用_c实例函数即render函数执行生成对应VNode对象
- patch阶段createComponent触发VNode init hook实现Vue实例创建
对于组件,v-model的处理实际上在render调用阶段是有相关处理的,实际上就是注册相应事件。
render函数执行阶段
无论是自定义组件还是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);
}
对于自定义组件会调用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;
}
}
从这里可以看出,当render函数调用期间会触发对子组件的创建,创建过程中会对v-model做事件定义:v-model默认的prop是value、event名称默认是input,其prop会保存到props属性中,事件与model对象上callback建立联系。
组件最终都会被转换成JSX数据对象形式,保存在虚拟对象VNode的data属性上
截止到这里,我们还没有看到事件被注册,只是看到事件并定义保存在虚拟对象VNode的data下on数据对象中。实际上这里on数据对象中保存的关于model对应的input/change事件只是临时保存的,看createComponent后面的逻辑就知道了,具体逻辑如下:
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
// 提出事件,这些事件应该作为子组件的事件,而不是原生DOM事件
var listeners = data.on;
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn;
// return a placeholder vnode
var name = Ctor.options.name || tag;
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
由上面逻辑可知数据对象on中事件都会被保存到虚拟对象VNode的componentOptions中,而数据对象on会被数据对象nativeOn覆盖掉。
此时v-model的事件定义都保存在componentOptions属性中,该属性时组件配置项,用于创建Vue实例的。那么事件是在哪里被注册的呢?
答案是在patch阶段createComponent,该函数会触发子组件的Vue实例化过程,则执行_init实例方法,而在该实例方法中有一个逻辑是至关重要的
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
}
所有通过$createElement创建的组件都会存在_isComponent这个属性,所以可知对于内部组件(即render函数阶段触发创建组件创建都看成是内联组件)会优化选项处理。
function initInternalComponent (vm, options) {
var opts = vm.$options = Object.create(vm.constructor.options);
// doing this because it's faster than dynamic enumeration.
var parentVnode = options._parentVnode;
var vnodeComponentOptions = parentVnode.componentOptions;
opts._parentListeners = vnodeComponentOptions.listeners;
}
function initEvents (vm) {
vm._events = Object.create(null);
vm._hasHookEvent = false;
// 对于render函数创建createElemnt
var listeners = vm.$options._parentListeners;
if (listeners) {
updateComponentListeners(vm, listeners);
}
}
为什么这里是parentVnode?
实际上是因为render函数中组件创建调用$createElement,而针对于自定义组件会调用createComponent函数,该函数内部对于会调用Vue.extend方法,所以会是parentVnode
实际上组件的v-model对应的事件会在这里被触发,即updateComponentListeners。
实际上可以大胆猜想,所有组件的更新操作都是通过updateComponentListeners函数来处理的
而updateComponentListeners最终还是调用updateListeners来实现事件更新操作,最终调用$on来实现组件事件的注册。
当emit相关事件,就会触发已经注册的v-model的input/change事件,从而实现双向绑定。
涉及特殊属性的v-model的特殊处理
首先明确下特殊属性:
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之外,主要分为:
- 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
会忽略所有表单元素的value
、checked
、selected
特性的初始值而总是将 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);
}
实际上从addHandler可以看到与组件v-model的区别了:
组件的v-model会在patch阶段组件Vue实例创建时触发updateComponentListeners来实现事件注册等操作,而表单元素的v-model直接在构建render函数阶段就已经注册了
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=$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绑定的是数组类型的值的特殊处理。
<template>
<div>
<input v-model="checkList" type="checkbox" :value="1">复选框A</>
<input v-model="checkList" type="checkbox" :value="2">复选框B</>
</div>
</template>
<script>
export default {
data() {
return {
checkList: [1]
};
}
}
</script>
// 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;
}
}
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 = 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 实例的数据作为数据来源。
表单元素和组件上应用v-model其背后的处理逻辑存在较大差异,最大的不同在于事件注册的时机(表单元素v-model事件注册在render函数生成阶段,而组件的v-model事件是在patch阶段,其vue实例创建时调用updateComponentListener实现的)
更多推荐
所有评论(0)