那么为什么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 }
参考资料:
https://cn.vuejs.org/v2/guide...
https://developer.mozilla.org...
https://developer.mozilla.org...