Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何理解vue中的v-model? #187

Open
FrankKai opened this issue Mar 18, 2020 · 0 comments
Open

如何理解vue中的v-model? #187

FrankKai opened this issue Mar 18, 2020 · 0 comments

Comments

@FrankKai
Copy link
Owner

FrankKai commented Mar 18, 2020

说到v-model,就想到了双向数据绑定,而且往往最常见的是在表单元素<input>,<textarea>,<select>中的使用,在一些自定义组件中也使用到了v-model。

那么为什么v-model双向数据绑定,自动更新元素呢?

v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text 和 textarea 元素使用 value 属性和 input 事件;
checkbox 和 radio 使用 checked 属性和 change 事件;
select 字段将 value 作为 prop 并将 change 作为事件。

因此我们将来好好探讨一下:

  • vue v-model源码分析
    • vue如何获取v-mode的值(value)
    • 用于生成各种类型model的gen*函数
    • vue如何设置v-mode的值(value)
  • <input v-model="..."><textarea v-model="..."></textarea>源码分析
  • <input type="checkbox" v-model="..."><input type="radio" v-model="...">源码分析
  • <select v-model="..."><option>...</option></select>源码分析
  • 自定义组件中的v-model源码分析
  • v-model的.lazy,.number,.trim修饰符源码分析

vue v-model源码分析

vue如何获取v-mode的值(value)

export default function model (
  el: ASTElement,
  dir: ASTDirective,
  _warn: Function
): ?boolean {
  warn = _warn
  const value = dir.value // vue通过这一行取到了v-model中的值,例如v-model="isChecked",v-model="inputValue"中isChecked和inputValue的值
...
}
const value = dir.value

vue通过这一行取到了v-model中的值,例如v-model="isChecked",v-model="inputValue"中isChecked和inputValue的值。
源码地址:web/compiler/directives/model.js 14~20行

其中ASTDirective的interface定义如下:

export interface ASTDirective {
  name: string;
  rawName: string;
  value: string; // 注意这里
  arg: string | undefined;
  modifiers: ASTModifiers | undefined;
}

源码地址:packages/vue-template-compiler/types/index.d.ts 80~86行

用于生成各种类型model的gen*函数

  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') {
    ...
  }

其中包括gen*开头的生成model的各种函数,我们会在下文中进行分析。
源码地址:web/compiler/directives/model.js 37~61行

vue如何设置v-mode的值(value)

genAssignmentCode函数(事件更新时,最终set v-model值的函数)

// 自定义component组件
const assignment = genAssignmentCode(value, valueExpression)
// input(text),textarea
let code = genAssignmentCode(value, valueExpression)
// <radio>
addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true)
// <select>
code = `${code} ${genAssignmentCode(value, assignment)}`
// <checkbox>
  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
  )
/**
 * Cross-platform codegen helper for generating v-model value assignment code.
 */
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})`
  }
}

源码地址:src/compiler/directives/model.js 36~46行

<input v-model="..."><textarea v-model="..."></textarea>源码分析

一句话概览:text和textarea标签的value属性和input事件及genDefaultModel函数。

<!--text类型-->
<input v-model="singleMsg">
<!--textarea类型-->
<textarea v-model="multiMsg"></textarea>

等价于:

<input type="text" v-bind:value="singleMsg" v-on:input="singleMsg=$event.target.value" >
<textarea v-bind:value="multiMsg" v-on:input="multiMsg=$event.target.value"></textarea>

真的是这样吗?我们来看看源码。

genDefaultModel函数

function genDefaultModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
): ?boolean {
  const type = el.attrsMap.type

  // warn if v-bind:value conflicts with v-model
  // except for inputs with v-bind:type
  if (process.env.NODE_ENV !== 'production') {
     ...
  }

  const { lazy, number, trim } = modifiers || {}
  const needCompositionGuard = !lazy && type !== 'range'
  const event = lazy // 此处我们没有传入lazy修饰符,因此event变量是'input'
    ? '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})`) // <input type="text" v-bind:value="singleMsg" ="singleMsg=$event.target.value" >
  addHandler(el, event, code, null, true) // 这一步印证了input事件<input v-on:input="...">
  if (trim || number) {
    addHandler(el, 'blur', '$forceUpdate()')
  }
}

源码分析在源码中有注释。

通过源码我们可以看出:
input(type=“text”)和textarea的v-model,通过value prop获得值,最终被解析为设置value attribute和input(若设置lazy,则触发change)事件,从而实现双向绑定。

源码地址:web/compiler/directives/model.js 127~147行

<input type="checkbox" v-model="..."><input type="radio" v-model="...">源码分析

一句话概览:input标签的checkbox和radio类型的checked属性和change事件及源码genCheckboxModel函数和genRadioModel函数。

<!--checkbox类型-->
<input type="checkbox" v-model="checkboxCtrl">
<!--input类型-->
<input type="radio" v-model="radioCtrl">

等价于:

<input type="checkbox" v-bind:value="checkboxCtrl" v-on:change="checkboxCtrl=$event.target.checked">
<input type="radio" v-bind:value="radioCtrl" v-on:change="radioCtrl=$event.target.checked">

genCheckboxModel函数

function genCheckboxModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
) {
  const number = modifiers && modifiers.number
  const valueBinding = getBindingAttr(el, 'value') || 'null' // 从v-bind的value获得到初始值,印证了
<input v-bind:value="...">
  const trueValueBinding = getBindingAttr(el, 'true-value') || 'true'
  const falseValueBinding = getBindingAttr(el, 'false-value') || 'false'
  addProp(el, 'checked', // 设置标签的checked attribute,印证了<input ="checkboxCtrl=$event.target.checked">
    `Array.isArray(${value})` +
    `?_i(${value},${valueBinding})>-1` + (
      trueValueBinding === 'true'
        ? `:(${value})`
        : `:_q(${value},${trueValueBinding})`
    )
  )
  addHandler(el, 'change', // 这一步印证了<input v-on: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
  )
}

genRadioModel函数

function genRadioModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
) {
  const number = modifiers && modifiers.number
  let valueBinding = getBindingAttr(el, 'value') || 'null' // 印证了
<input v-bind:value="...">
  valueBinding = number ? `_n(${valueBinding})` : valueBinding
  addProp(el, 'checked', `_q(${value},${valueBinding})`)  // 设置标签的checked attribute,印证了<input ="radioCtrl=$event.target.checked">
  addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true) // 这一步印证了<input v-on:change="...">
}

源码分析在源码中有注释。

通过源码我们可以看出:
input(type=“checkbox”)和input(type="radio")的v-model,通过value prop获得值,最终被解析为设置checked attribute和change事件,从而实现双向绑定。

源码地址:web/compiler/directives/model.js 67~96行

<select v-model="..."><option>...</option></select>源码分析

一句话概览:select标签的value属性和change事件及源码genSelect函数。

<!--select类型-->
  <select v-model="selectCtrl">
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>

等价于:

  <select v-bind:value="selectValue" v-on:change="selectValue=$event.target.options.selected">
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>

genSelect 函数

function genSelect (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
) {
  const number = modifiers && modifiers.number
  // 这一步印证了<select v-bind:value="selectValue" ...="selectValue=$event.target.options.selected">
  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)}`
  // 这一步印证了 <select v-on:change="...">
  addHandler(el, 'change', code, null, true)
}

源码分析在源码中有注释。

通过源码我们可以看出:
select的v-model,通过value prop获得值,最终被解析为设置selected attribute和change事件,从而实现双向绑定。

源码地址:web/compiler/directives/model.js 110~125行

自定义组件中的v-model源码分析

一句话概览:如何理解自定义组件中的v-model及genComponentModel,transformModel函数?

其实不仅仅是input,textarea,和select这些常见的原生表单元素可以使用v-model,对于自定义的vue组件我们也可以使用v-model。

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})
<base-checkbox v-model="lovingVue"></base-checkbox>

lovingVue的值会传入checked这个prop,同时当组件触发一个change事件并且附带了一个新值的时候,lovingVue的值会被更新。

如何从源码去理解这一段话吗?

genComponentModel函数(构造出需要调用transformModel函数的el.model对象)

export function genComponentModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
): ?boolean {
  const { number, trim } = modifiers || {}
  ...
  const assignment = genAssignmentCode(value, valueExpression)
  el.model = {
    value: `(${value})`,
    expression: JSON.stringify(value),
    callback: `function (${baseValueExpression}) {${assignment}}`
  }
}

源码地址:src/compiler/directives/model.js 6~31行
重点关注el.model这一行。因为vue在创建自定义组件时,会对el.model这一行进行一些操作。

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

transformModel函数(转化组件v-model的value和callback到prop和event handler)

// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel (options, data: any) {
  const prop = (options.model && options.model.prop) || 'value' //   model: {prop: 'checked',event:'change'} 中的”checked“作为prop
  const event = (options.model && options.model.event) || 'input' // model: {prop: 'checked',event:'change'} 中的”change“作为event
  ;(data.attrs || (data.attrs = {}))[prop] = data.model.value // lovingVue的值会传入checked这个prop
  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 // "同时当组件触发一个change事件并且附带了一个新值的时候,lovingVue的值会被更新。"
  }
}

源码地址:src/core/vdom/create-component.js 250~268行
”lovingVue的值会传入checked这个prop“的源码解释:
data.attrs || (data.attrs = {}))[prop] = data.model.value
获取到组价的v-model="lovingVue"中lovingVue的值,并且将v-model的longVue传入props的checked。若对如何获取v-model中的值有不理解,具体可以看上文第一部分vue v-model源码分析(vue如何获取v-mode的值(value))。

"同时当组件触发一个change事件并且附带了一个新值的时候,lovingVue的值会被更新。"的源码解释:
通过el.model = {... callback: }const callback = data.model.callback可以推断出:el.on.change = callback,而这个callback又是genAssignmentCode,在genAssignmentCode中会对v-model的值进行set。若是对如何set v-model的值有疑问,可以查看上文中的vue v-model源码分析(vue如何设置v-mode的值(value))。

v-model的.lazy,.number,.trim修饰符源码分析

const { lazy, number, trim } = modifiers || {}
...
  const event = lazy
    ? 'change'
    : type === 'range'
      ? RANGE_TOKEN
      : 'input'

  ...
  if (trim) {
    valueExpression = `$event.target.value.trim()`
  }
  if (number) {
    valueExpression = `_n(${valueExpression})`
  }
  • lazy仅仅是一个判断触发标签change还是input事件的标识符,lazy为true时,触发change
  • trim仅仅是调用了String.prototype.trim()
  • number这是vue的_n函数,而这个函数其实是一个StringToNumber函数,相当简单
/**
 * Convert an input value to a number for persistence.
 * If the conversion fails, return original string.
 */
export function toNumber (val: string): number | string {
  const n = parseFloat(val)
  return isNaN(n) ? val : n
}

源码地址:shared/util.js 97~100行

参考资料:
https://cn.vuejs.org/v2/guide/components.html#%E5%9C%A8%E7%BB%84%E4%BB%B6%E4%B8%8A%E4%BD%BF%E7%94%A8-v-model
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant