Vue之v-model

前言

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

本文的目标有两个:

  • v-model语法糖的实现逻辑
  • 涉及特殊属性的v-model的特殊处理

v-model实现逻辑

从简单实例出发,来梳理v-model的处理逻辑:

实际上这里只需要从processAttrs中的逻辑开始即可,前面的处理实际上之前的文章都有说明,这里就不在描述了(可查看数据对象这篇文章)。

解析阶段

v-model虽然是Vue提供的语法糖,从类型上来说是Vue指令,对解析器来来说都是属性。所有属性的而处理都是通过processAttrs来处理的。

Vue之v-model_第1张图片
上图中就是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_第2张图片
从上面逻辑可知,对于组件来说最后需要调用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=$$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 = 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=$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 = 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 实例的数据作为数据来源。

这里总结下Vue中在整个数据对象解析过程中主要的节点:

  • pasre阶段(即调用parseHTML函数,只有通过template形式才会存在)的handleSatrtTag的处理逻辑,涉及到slot、v-for、v-if等所有作为HTML特性的解析处理,其中processAttrs就是普通属性和v-on、v-bind、:的处理逻辑
  • render函数构建阶段(即调用generate函数,只有通过template形式才会存在)的genElement的主要处理逻辑,这里涉及到所有的数据对象的形式构建,用于组成render函数
  • render函数执行阶段,即主要是$createElement中的主要处理来生成VNode

你可能感兴趣的:(Vue相关)