如何理解vue中的v-model?

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

image.png


那么为什么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...

https://developer.mozilla.org...

https://developer.mozilla.org...


相关文章
|
8月前
|
JavaScript
Vue中如何实现兄弟组件之间的通信
在Vue中,兄弟组件可通过父组件中转、事件总线、Vuex/Pinia或provide/inject实现通信。小型项目推荐父组件中转或事件总线,大型项目建议使用Pinia等状态管理工具,确保数据流清晰可控,避免内存泄漏。
682 2
|
7月前
|
缓存 JavaScript
vue中的keep-alive问题(2)
vue中的keep-alive问题(2)
585 137
|
11月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
1131 0
|
11月前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能
|
10月前
|
人工智能 JSON JavaScript
VTJ.PRO 首发 MasterGo 设计智能识别引擎,秒级生成 Vue 代码
VTJ.PRO发布「AI MasterGo设计稿识别引擎」,成为全球首个支持解析MasterGo原生JSON文件并自动生成Vue组件的AI工具。通过双引擎架构,实现设计到代码全流程自动化,效率提升300%,助力企业降本增效,引领“设计即生产”新时代。
694 1
|
11月前
|
JavaScript 前端开发 开发者
Vue 自定义进度条组件封装及使用方法详解
这是一篇关于自定义进度条组件的使用指南和开发文档。文章详细介绍了如何在Vue项目中引入、注册并使用该组件,包括基础与高级示例。组件支持分段配置(如颜色、文本)、动画效果及超出进度提示等功能。同时提供了完整的代码实现,支持全局注册,并提出了优化建议,如主题支持、响应式设计等,帮助开发者更灵活地集成和定制进度条组件。资源链接已提供,适合前端开发者参考学习。
698 17
|
10月前
|
JavaScript 安全
在 Vue 中,如何在回调函数中正确使用 this?
在 Vue 中,如何在回调函数中正确使用 this?
496 0
|
11月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
730 1
|
11月前
|
存储 JavaScript 前端开发
如何高效实现 vue 文件批量下载及相关操作技巧
在Vue项目中,实现文件批量下载是常见需求。例如文档管理系统或图片库应用中,用户可能需要一次性下载多个文件。本文介绍了三种技术方案:1) 使用`file-saver`和`jszip`插件在前端打包文件为ZIP并下载;2) 借助后端接口完成文件压缩与传输;3) 使用`StreamSaver`解决大文件下载问题。同时,通过在线教育平台的实例详细说明了前后端的具体实现步骤,帮助开发者根据项目需求选择合适方案。
971 0
|
11月前
|
JavaScript 前端开发 UED
Vue 项目中如何自定义实用的进度条组件
本文介绍了如何使用Vue.js创建一个灵活多样的自定义进度条组件。该组件可接受进度段数据数组作为输入,动态渲染进度段,支持动画效果和内容展示。当进度超出总长时,超出部分将以红色填充。文章详细描述了组件的设计目标、实现步骤(包括props定义、宽度计算、模板渲染、动画处理及超出部分的显示),并提供了使用示例。通过此组件,开发者可根据项目需求灵活展示进度情况,优化用户体验。资源地址:[https://pan.quark.cn/s/35324205c62b](https://pan.quark.cn/s/35324205c62b)。
526 0