如何理解vue中的v-model?

說到v-model,就想到了雙向數據綁定,並且每每最多見的是在表單元素<input>,<textarea>,<select>中的使用。html

那麼爲何v-model雙向數據綁定,自動更新元素呢?前端

v-model 在內部爲不一樣的輸入元素使用不一樣的屬性並拋出不一樣的事件:
text 和 textarea 元素使用 value 屬性和 input 事件;
checkbox 和 radio 使用 checked 屬性和 change 事件;
select 字段將 value 做爲 prop 並將 change 做爲事件。

所以咱們未來好好探討一下:vue

  • vue v-model源碼分析
  • text和textarea標籤的value屬性和input事件及源碼genDefaultModel函數
  • input標籤的checkbox和radio類型的checked屬性和change事件及源碼genCheckboxModel函數和genRadioModel函數
  • select標籤的value屬性和change事件及源碼genSelect函數
  • v-model的.lazy,.number,.trim修飾符源碼分析

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

源碼地址:web/compiler/directives/model.js 37~61行git

text和textarea標籤的value屬性和input事件及genDefaultModel函數

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

等價於:github

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

真的是這樣嗎?咱們來看看源碼。web

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()')
  }
}

源碼分析在源碼中有註釋。segmentfault

經過源碼咱們能夠看出:
input(type=「text」)和textarea的v-model,經過value prop得到值,最終被解析爲設置value attribute和input(若設置lazy,則觸發change)事件,從而實現雙向綁定。微信

源碼地址:web/compiler/directives/model.js 127~147行前端工程師

input標籤的checkbox和radio類型的checked屬性和change事件及源碼genCheckboxModel函數和genRadioModel函數

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

等價於:ide

<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標籤的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的.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...
https://developer.mozilla.org...
https://developer.mozilla.org...

期待和你們交流,共同進步,歡迎你們加入我建立的與前端開發密切相關的技術討論小組:

努力成爲優秀前端工程師!

相關文章
相關標籤/搜索