v-mode在不同表单标签上的实现

216 阅读2分钟

前言

今天,vue的v-model在不同标签上,编译的时候做了不一样的处理,现在来一起了解一下把

v-model的编译处理

  • 定义在组件上
  • 定义在input下拉选择上
  • 定义在input多选上
  • 定义在input单选上
  • 定义在input多行文本输入框上
export default function model (
  el: ASTElement,
  dir: ASTDirective,
  _warn: Function
): ?boolean {
  const value = dir.value
  const modifiers = dir.modifiers
  const tag = el.tag
  const type = el.attrsMap.type

  if (el.component) {
    genComponentModel(el, value, modifiers)
    return false
  } else if (tag === 'select') {
    genSelect(el, value, modifiers)
  } else if (tag === 'input' && type === 'checkbox') {
    genCheckboxModel(el, value, modifiers)
  } else if (tag === 'input' && type === 'radio') {
    genRadioModel(el, value, modifiers)
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers)
  } else if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers)
    return false
  }
  return true
}

v-model在组件上

  • 根据v-model的修饰符是number/trim,将字符串案规则处理
  • 生成callback回调函数注册成自定义事件,将value=v-model值
  • 绑定input自定义事件为callback,触发时给v-model重新赋值
function genComponentModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
): ?boolean {
  const { number, trim } = modifiers || {}

  const baseValueExpression = '$$v'
  let valueExpression = baseValueExpression
  if (trim) {
    valueExpression =
      `(typeof ${baseValueExpression} === 'string'` +
      `? ${baseValueExpression}.trim()` +
      `: ${baseValueExpression})`
  }
  if (number) {
    valueExpression = `_n(${valueExpression})`
  }
  // 将表达式赋值给value
  const assignment = genAssignmentCode(value, valueExpression)

  el.model = {
    value: `(${value})`,
    expression: JSON.stringify(value),
    callback: `function (${baseValueExpression}) {${assignment}}`
  }
}

// 编辑完生成的代码
with(this) {
  return _c('haha', {
    model: {
      value: (test),
      callback: function($$v) {
        test = $$v
      },
      expression: "test"
    }
  })
}

// 创建组件时,对model进行处理
function transformModel(options, data: any) {
  const prop = (options.model && options.model.prop) || 'value'
  const event = (options.model && options.model.event) || 'input';
  (data.attrs || (data.attrs = {}))[prop] = data.model.value
  const on = data.on || (data.on = {})
  const existing = on[event]
  const callback = data.model.callback
  if (isDef(existing)) {
    if (
      Array.isArray(existing) ?
      existing.indexOf(callback) === -1 :
      existing !== callback
    ) {
      on[event] = [callback].concat(existing)
    }
  } else {
    on[event] = callback
  }
}

v-model在select标签上

  • 将下拉组件中选中的选项的值处理成数组
  • 注册change事件,下拉更改后重新给value赋值
function genSelect (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
) {
  const number = modifiers && modifiers.number
  // 将选中的下拉处理成数组
  const 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'}})`

  const assignment = '$event.target.multiple ? $$selectedVal : $$selectedVal[0]'
  let code = `var $$selectedVal = ${selectedVal};`
  code = `${code} ${genAssignmentCode(value, assignment)}`
  addHandler(el, 'change', code, null, true)
}

// 编译后的代码
with(this) {
  return _c('select', {
    directives: [{
      name: "model",
      rawName: "v-model",
      value: (test),
      expression: "test"
    }],
    on: {
      "change": function($event) {
        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 val
          });
        test = $event.target.multiple ? $$selectedVal : $$selectedVal[0]
      }
    }
  })
}

v-model在input的多选框上

  • 获取value、true-value、false-value的值
  • 根据v-model是否是数组,input框的value的值,判断多选框是否选中
  • 绑定change事件,触发了对v-model重新赋值
function genCheckboxModel (
  el: ASTElement,
  value: string, // v-model的值
  modifiers: ?ASTModifiers
) {
  const number = modifiers && modifiers.number
  const valueBinding = getBindingAttr(el, 'value') || 'null'
  const trueValueBinding = getBindingAttr(el, 'true-value') || 'true'
  const falseValueBinding = getBindingAttr(el, 'false-value') || 'false'
  addProp(el, 'checked',
    `Array.isArray(${value})` +
    `? 
    _i(${value},${valueBinding}) > -1` + (
      trueValueBinding === 'true'
        ? `:(${value})` // 直接返回value的值
        : `:_q(${value},${trueValueBinding})` // 返回布尔值
    )
  )
  
  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
  )
}

// 编译后的代码
with(this) {
  return _c('input', {
    directives: [{
      name: "model",
      rawName: "v-model",
      value: (test),
      expression: "test"
    }],
    attrs: {
      "type": "checkbox"
    },
    domProps: {
      "checked": Array.isArray(test) ? _i(test, null) > -1 : (test)
    },
    on: {
      "change": function($event) {
        var $$a = test,
          $$el = $event.target,
          $$c = $$el.checked ? (true) : (false);
        if (Array.isArray($$a)) {
          var $$v = null,
            $$i = _i($$a, $$v); // $$v在数组中的索引
          // 选中的话,数组中能含该项;没选中,不能有
          if ($$el.checked) {
            $$i < 0 && (test = $$a.concat([$$v]))
          } else {
            $$i > -1 && (test = $$a.slice(0, $$i)
              .concat($$a.slice($$i + 1)))
          }
        } else {
          // 不是数组,直接赋值
          test = $$c
        }
      }
    }
  })
}

v-model在input的单选框上

  • 直接取出输入框的value的值
  • change事件时,赋值给v-model
function genRadioModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
) {
  const number = modifiers && modifiers.number
  let valueBinding = getBindingAttr(el, 'value') || 'null'
  valueBinding = number ? `_n(${valueBinding})` : valueBinding
  addProp(el, 'checked', `_q(${value},${valueBinding})`)
  addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true)
}

// 编译后的代码
with(this) {
  return _c('input', {
    directives: [{
      name: "model",
      rawName: "v-model",
      value: (test),
      expression: "test"
    }],
    attrs: {
      "type": "radio"
    },
    domProps: {
      "checked": _q(test, null)
    },
    on: {
      "change": function($event) {
        test = null
      }
    }
  })
}

标签是input或者textarea

function genDefaultModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
): ?boolean {
  const type = el.attrsMap.type
  const { lazy, number, trim } = modifiers || {}
  const needCompositionGuard = !lazy && type !== 'range'
  const event = lazy
    ? 'change'
    : type === 'range'
      ? RANGE_TOKEN
      : 'input'

  let valueExpression = '$event.target.value'
  if (trim) {
    valueExpression = `$event.target.value.trim()`
  }
  if (number) {
    valueExpression = `_n(${valueExpression})`
  }

  let code = genAssignmentCode(value, valueExpression)
  if (needCompositionGuard) {
    code = `if($event.target.composing)return;${code}`
  }

  addProp(el, 'value', `(${value})`)
  addHandler(el, event, code, null, true)
  if (trim || number) {
    addHandler(el, 'blur', '$forceUpdate()')
  }
}

最后

v-model的在input的type属性不一样时,处理的方式不一样,尤其在type=checkbox时,可以支持值是一个数组,这个功能一直以为是element-ui处理,其实就是vue做的处理。所以,多看源码才能更加了解vue的使用的细节。欢迎大家点赞收藏~