如何理解vue中的v-model?

说到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
获取到<base-checkbox>组价的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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,227评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,755评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,899评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,257评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,617评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,757评论 1 221
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,982评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,715评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,454评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,666评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,148评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,512评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,156评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,112评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,896评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,809评论 2 279
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,691评论 2 272

推荐阅读更多精彩内容

  • 目标:1、了解v-model的本质。2、了解v-model的实现原理。 我们知道Vue的核心特性之一是双向绑定,v...
    LoveBugs_King阅读 222,287评论 3 38
  • v-model是什么? v-model是vue给我们提供的一个内置指令(多用于表单组件的“双向数据绑定”),而内置...
    FTD止水阅读 3,286评论 0 0
  • 抛出问题 我们先来看一下下面这段代码 上述代码很简单,就不做过多的解释了。如果这段代码都看不懂,那下面也没必要再看...
    var_Change阅读 2,162评论 0 2
  • 以下内容是我在学习和研究Vue时,对Vue的特性、重点和注意事项的提取、精练和总结,可以做为Vue特性的字典; 1...
    科研者阅读 13,957评论 3 24
  • Vue 实例 属性和方法 每个 Vue 实例都会代理其 data 对象里所有的属性:var data = { a:...
    云之外阅读 2,127评论 0 6