Vue2.x 源码 - v-model

上一篇:Vue2.x 源码 - event

v-model 是 Vue 提供的双向绑定(数据驱动 DOM,DOM 变化影响数据)的全局指令,可以作用在表单元素上以及组件上;

v-model 解析

1、在v-modelparse解析阶段,它会在processElement方法中调用processAttrs来处理标签上面解析的各种属性;在 src/compiler/parser/index.js 文件中:

export const dirRE = process.env.VBIND_PROP_SHORTHAND
  ? /^v-|^@|^:|^\.|^#/
  : /^v-|^@|^:|^#/
const argRE = /:(.*)$/
function processAttrs (el) {
  const list = el.attrsList
  let i, l, name, rawName, value, modifiers, syncGen, isDynamic
  for (i = 0, l = list.length; i < l; i++) {
    name = rawName = list[i].name
    value = list[i].value
    if (dirRE.test(name)) {
      el.hasBindings = true
      // modifiers省略代码
      if (bindRE.test(name)) {
        // v-bind省略代码
      } else if (onRE.test(name)) {
        // v-on省略代码
      } else {
        // normal directives
        name = name.replace(dirRE, '')
        // parse arg
        const argMatch = name.match(argRE)
        let arg = argMatch && argMatch[1]
        isDynamic = false
        if (arg) {
          name = name.slice(0, -(arg.length + 1))
          if (dynamicArgRE.test(arg)) {
            arg = arg.slice(1, -1)
            isDynamic = true
          }
        }
        addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
        if (process.env.NODE_ENV !== 'production' && name === 'model') {
          checkForAliasModel(el, value)
        }
      }
    } else {
      // ...省略代码
    }
  }
}

processAttrs 方法首先使用dirRE正则表达式把v-model字符串中的v-前缀去掉,此时name的值就变成了model;紧接着,它又使用了argRE正则表达式来匹配指令参数,处理完毕后,调用addDirective方法,给ast对象添加directives属性:

export function addDirective (
  el: ASTElement,
  name: string,
  rawName: string,
  value: string,
  arg: ?string,
  isDynamicArg: boolean,
  modifiers: ?ASTModifiers,
  range?: Range
) {
  (el.directives || (el.directives = [])).push(rangeSetItem({
    name,
    rawName,
    value,
    arg,
    isDynamicArg,
    modifiers
  }, range))
  el.plain = false
}

2、接下来在codegen代码生成阶段,会在genData方法中调用genDirectives来处理指令,在 src/compiler/codegen/index.js 中:

function genDirectives (el: ASTElement, state: CodegenState): string | void {
  const dirs = el.directives
  if (!dirs) return
  let res = 'directives:['
  let hasRuntime = false
  let i, l, dir, needRuntime
  for (i = 0, l = dirs.length; i < l; i++) {
    dir = dirs[i]
    needRuntime = true
    const gen: DirectiveFunction = state.directives[dir.name]
    if (gen) {
      // 操作AST的编译时指令,如果它也需要运行时对应的对象,则返回true
      needRuntime = !!gen(el, dir, state.warn)
    }
    if (needRuntime) {
      hasRuntime = true
      res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
        dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
      }${
        dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
      }${
        dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
      }},`
    }
  }
  if (hasRuntime) {
    return res.slice(0, -1) + ']'
  }
}

genDrirectives ⽅法就是遍历 el.directives ,然后获取每⼀个指令对应的⽅法 state.directives[dir.name] ,这个指令⽅法实际上是在实例化 CodegenState 的时候通过 option 传⼊的,这个 option 就是编译相关的配置,它在不同的平台 下配置不同,在 web 环境下的定义在 src/platforms/web/compiler/options.js 下:

import directives from './directives/index'
export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules,
  directives,
  isPreTag,
  isUnaryTag,
  mustUseProp,
  canBeLeftOpenTag,
  isReservedTag,
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
}

directives 定义在 src/platforms/web/compiler/directives/index.js 中:

export default { 
	model, 
	text, 
	html 
}

那么对于 v-model ⽽⾔,对应的 directive 函数是在 src/platforms/web/compiler/directives/model.js 中定义的 model 函数:

export default function model (
  el: ASTElement,
  dir: ASTDirective,
  _warn: Function
): ?boolean {
  warn = _warn
  const value = dir.value
  const modifiers = dir.modifiers
  const tag = el.tag
  const type = el.attrsMap.type
  if (process.env.NODE_ENV !== 'production') {
    // inputs with type="file" are read only and setting the input's
    // value will throw an error.
    if (tag === 'input' && type === 'file') {
      warn(
        `<${el.tag} v-model="${value}" type="file">:\n` +
        `File inputs are read only. Use a v-on:change listener instead.`,
        el.rawAttrsMap['v-model']
      )
    }
  }
  if (el.component) {
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    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)
    // component v-model doesn't need extra runtime
    return false
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `<${el.tag} v-model="${value}">: ` +
      `v-model is not supported on this element type. ` +
      'If you are working with contenteditable, it\'s recommended to ' +
      'wrap a library dedicated for that purpose inside a custom component.',
      el.rawAttrsMap['v-model']
    )
  }
  // ensure runtime directive metadata
  return true
}

model方法中,首先判断如果在type='file'input标签上使用了v-model,那么会在开发环境提示错误信息,因为附件是只读的;随后根据标签类型,分别调用对应的方法,v-model 指令会命中genDefaultModel

function genDefaultModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
): ?boolean {
  const type = el.attrsMap.type
  if (process.env.NODE_ENV !== 'production') {
    const value = el.attrsMap['v-bind:value'] || el.attrsMap[':value']
    const typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']
    if (value && !typeBinding) {
      const binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'
      warn(
        `${binding}="${value}" conflicts with v-model on the same element ` +
        'because the latter already expands to a value binding internally',
        el.rawAttrsMap[binding]
      )
    }
  }
  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()')
  }
}

genDefaultModel 函数先处理了 modifiers ,它的不同主要影响的是 eventvalueExpression 的值;然后去执⾏ genAssignmentCode 去⽣成代码,它的定义在 src/compiler/directives/model.js 中:

export function genAssignmentCode (
  value: string,
  assignment: string
): string {
  const res = parseModel(value)
  if (res.key === null) {
    return `${value}=${assignment}`
  } else {
    return `$set(${res.exp}, ${res.key}, ${assignment})`
  }
}

方法⾸先对 v-model 对应的 value 做了解析,最后根据res.key 来拼接并返回不同的 code

code ⽣成完后,⼜执⾏了 2 句⾮常关键的代码:

addProp:调用addProp是为了给ast添加一个值为valueprops属性;
addHandler:调用addHandler是为了给ast添加一个事件监听,至于到底监听什么事件取决于v-model作用于什么标签;

从以上分析中我们可以看出来:v-model处理双向绑定,本质上就是一种语法糖,它负责监听用户的输入事件然后更新数据,并对一些极端场景做了一些特殊处理

回到 genData 方法中,执行完 genDirectives 之后,当前ast对象中多了propsevents属性;

绑定表单元素

v-model 可以绑定的表单元素有 select 、input、textarea、radio、checkbox,在上面介绍 v-model 解析 的时候基本上就已经包括了这部分的内容,这里就根据具体表单元素说明一下它的流程(以 input、textarea 为例);

1、parse 解析阶段将 v-model 指令保存到 astdirective 数组中;
2、在codegen 代码生成阶段,调用genData来处理directives指令、props属性以及events事件;

1、首先调用 genDirectives 方法来处理指令,在这个方法中会调用一个与平台相关的 model 方法,在这个 model 方法中会根据不同元素标签的类型来分别处理;
2、然后调用 genDefaultModel 方法,它主要做四件事情:异常处理、修饰符处理、添加 props 属性以及添加 event 事件;

绑定组件

父组件

<template>
	<child v-model='msg'></child>
</template>
<script>
	import child from './child.vue';
	export default {
		components:{ child };
		data(){
			return {
				msg:0
			}
		}
	}
</script>

子组件

<template>
	<div>
		<el-input :value='msg' @input='change($event)'></el-input>
	</div>
</template>
<script>
	export default {
		name:'child',
		props:{
			msg:[Number,String]
		},
		methods:{
			change(e){
				this.$emit('input',e);
			}
		}
	}
</script>

1、在父组件编译阶段,会解析 v-model 指令,依然会执⾏ genData 函数中的 genDirectives 函数接着执⾏src/platforms/web/compiler/directives/model.js 中定义的 model 函数,并命中如下逻辑:

if (el.component) {
  genComponentModel(el, value, modifiers)
  //组件v-model不需要额外的运行
  return false
}

genComponentModel 函数定义在 src/compiler/directives/model.js 中:

export 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})`
  }
  const assignment = genAssignmentCode(value, valueExpression)
  el.model = {
    value: `(${value})`,
    expression: JSON.stringify(value),
    callback: `function (${baseValueExpression}) {${assignment}}`
  }
}

调用genComponentModel方法后,当前ast对象多了一个model属性;

2、然后在创建⼦组件 vnode 阶段,会执⾏ createComponent 函数,它的定义在 src/core/vdom/create-component.js 中:

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // ...省略代码
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }
  // ...省略代码
}

当执行到createComponent方法的时候,由于我们存在model属性,所以会调用transformModel来处理这部分的逻辑,我们来看一下transformModel这个方法的代码:

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
  }
}

transformModel 逻辑很简单,给 data.props 添加 data.model.value ,并且给 data.on 添加 data.model.callback

回到最上面的父子组件传值案例:

1、父组件通过 prop 传值给子组件;
2、子组件通过 input 事件出发子组件值的变化,然后调用 $emit 传递 input 事件将值传递给父组件;
3、父组件通过 imput 事件来接收子组件传递过来的值;

修饰符

.number和.trim修饰符
对于.number修饰符和.trim修饰符的处理非常简单,在genDefaultModel方法中其逻辑如下(其它地方处理过程类似):

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

当提供了.number修饰符时,使用了_n工具函数进行包裹,_n工具函数就是toNumber方法的缩写形式;

.lazy修饰符
lazy的作用是:在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步。你可以添加 lazy 修饰符,从而转为在 change 事件之后进行同步;

codegen代码生成后,它们生成的render函数on事件部分分别如下:

// normal
const normalRender = `
  on:{
    input:function($event){
      if($event.target.composing)return;
      msg=$event.target.value
    }
  }
`
// lazy
const lazyRender = `
  on:{
    change:function($event){
      msg=$event.target.value
    }
  }
`

正如官网介绍中的那样,使用lazy修饰符后,它由监听input事件变成了监听change事件;

总结

v-model 的实现需要一个属性和一个事件监听;它的本质是一种语法糖,可以支持原生表单元素,也可以支持自定义组件;

下一篇:Vue2.x 源码 - ref 和 $refs添加链接描述

你可能感兴趣的:(vue,vue.js,v-model,修饰符)