v-model is a directive provided by Vue, its main function is to establish two-way data binding on form elements <input>, <textarea>, <select>, and components. Essentially, it is a syntactic sugar that can be directly defined on native form elements and also supports custom components. In the implementation of components, it is possible to configure the prop names the child component receives, and the event names dispatched to achieve two-way binding in the component.
The v-model directive can be used to create two-way data binding on form elements <input>, <textarea>, and <select>, and it will automatically select the correct method to update the element according to the control type, using <input> as an example for using v-model.
<!DOCTYPEhtml><html><head><title>Vue</title></head><body><divid="app"></div></body><scriptsrc="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script><scripttype="text/javascript">var vm =newVue({el:"#app",data:{msg:""},template:` <div>
<div>Message is: {{ msg }}</div>
<input v-model="msg">
</div>
`})</script></html>
When not using the v-model syntactic sugar, you can implement two-way binding on your own. In fact, v-model internally uses different property and emits different events for different input elements:
input and textarea elements use the value property and input event.
checkbox and radio elements use the checked property and change event.
select elements take value as prop and use change event.
Again using <input> as an example, implementing two-way binding without using v-model.
<!DOCTYPEhtml><html><head><title>Vue</title></head><body><divid="app"></div></body><scriptsrc="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script><scripttype="text/javascript">var vm =newVue({el:"#app",data:{msg:""},template:` <div>
<div>Message is: {{ msg }}</div>
<input :value="msg" @input="msg = $event.target.value">
</div>
`})</script></html>
There are also modifiers for v-model to control user input:
.trim: Trims the input of leading and trailing whitespace.
.lazy: Listens to the change event instead of replacing the input event.
.number: Converts the input string to a valid number. If this value cannot be parsed by parseFloat(), the original value will be returned.
<!DOCTYPEhtml><html><head><title>Vue</title></head><body><divid="app"></div></body><scriptsrc="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script><scripttype="text/javascript">var vm =newVue({el:"#app",data:{msg:0},template:` <div>
<div>Message is: {{ msg }}</div>
<div>Type is: {{ typeof(msg) }}</div>
<input v-model.number="msg" type="number">
</div>
`})</script></html>
When using custom components, the v-model on the component will by default use a prop named value and an event named input. However, input controls like radio buttons and checkboxes may use the value attribute for different purposes. In such cases, the model option can be used to avoid such conflicts.
The implementation of Vue source code is quite complex, handling various compatibility issues, exceptions, and conditional branches. This article analyzes the core part of the code, a simplified version with important parts annotated. The commit id is ef56410.
v-model is a directive of Vue, so the analysis starts from the compilation phase. Before parsing the directive, the general flow of Vue compilation phase includes: parsing template strings to generate AST, optimizing the syntax tree AST, and generating the render string.
The handling of directives occurs during the process of generating the render string, i.e., in the generate function, calling genElement -> genData -> genDirectives. This article mainly analyzes the genDirectives function.
// dev/src/compiler/codegen/index.js line 55exportfunctiongenElement(el: ASTElement,state: CodegenState): string {// ... data =genData(el, state)// ...}// dev/src/compiler/codegen/index.js line 219exportfunctiongenData(el: ASTElement,state: CodegenState): string {// ...const dirs =genDirectives(el, state)// ...}
During the AST generation phase, also known as the parse phase, v-model is parsed as a regular directive into el.directives. The genDrirectives method iterates through el.directives and retrieves the method corresponding to each directive. For v-model, at this point, {name: "model", rawName: "v-model" ...} is obtained, and the method corresponding to the model directive is found through state, and is then executed.
// dev/src/compiler/codegen/index.js line 309functiongenDirectives(el: ASTElement,state: CodegenState): string |void{const dirs = el.directives // Obtain directivesif(!dirs)returnlet res ='directives:['let hasRuntime =falselet i, l, dir, needRuntime
for(i =0, l = dirs.length; i < l; i++){// Iterate through directives dir = dirs[i] needRuntime =trueconstgen: DirectiveFunction = state.directives[dir.name]// For v-model, const gen = state.directives["model"];if(gen){// compile-time directive that manipulates AST.// returns true if it also needs a runtime counterpart. needRuntime =!!gen(el, dir, state.warn)}if(needRuntime){ hasRuntime =true res +=`{name:"${dir.name}",rawName:"${dir.rawName}"${ dir.value ?`,value:(${dir.value}),expression:${JSON.stringify(dir.value)}`:''}${ dir.arg ?`,arg:${dir.isDynamicArg ? dir.arg :`"${dir.arg}"`}`:''}${ dir.modifiers ?`,modifiers:${JSON.stringify(dir.modifiers)}`:''}},`}}if(hasRuntime){return res.slice(0,-1)+']'}}
The model method mainly determines the type of tag based on the input parameters and calls different processing logic.
// dev/src/platforms/web/compiler/directives/model.js line 14exportdefaultfunctionmodel(el: ASTElement,dir: ASTDirective,_warn: Function):?boolean { warn = _warn
const value = dir.value
const modifiers = dir.modifiers
const tag = el.tag
const type = el.attrsMap.type
if(process.env.NODE_ENV!=='production'){// If the environment is not in production, we need to perform the following checks:// 1. Inputs with type="file" are read-only, and setting the input value will throw an error.if(tag ==='input'&& type ==='file'){warn(`<${el.tag} v-model="${value}" type="file">:\n`+`File inputs are read-only. Use a v-on:change listener instead.`, el.rawAttrsMap['v-model'])}}// Branch handlingif(el.component){genComponentModel(el, value, modifiers)// The component v-model doesn't need extra runtimereturnfalse}elseif(tag ==='select'){genSelect(el, value, modifiers)}elseif(tag ==='input'&& type ==='checkbox'){genCheckboxModel(el, value, modifiers)}elseif(tag ==='input'&& type ==='radio'){genRadioModel(el, value, modifiers)}elseif(tag ==='input'|| tag ==='textarea'){genDefaultModel(el, value, modifiers)}elseif(!config.isReservedTag(tag)){genComponentModel(el, value, modifiers)// The component v-model doesn't need extra runtimereturnfalse}elseif(process.env.NODE_ENV!=='production'){warn(`<${el.tag} v-model="${value}">: `+`v-model is not supported on this element type. `+'If you are working with contenteditable, it\'s recommended to '+'wrap a library dedicated for that purpose inside a custom component.', el.rawAttrsMap['v-model'])}// Ensure runtime directive metadatareturntrue}
The genDefaultModel function first handles the modifiers. The different modifiers mainly affect the values of event and valueExpression. For the <input> tag, the event is input, and the valueExpression is $event.target.value. Then, it executes genAssignmentCode to generate code and add the attribute value and event handling.
// dev/src/platforms/web/compiler/directives/model.js line 127functiongenDefaultModel(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:typewarn if v-bind:value conflicts with v-model except for inputs with v-bind:type
if(process.env.NODE_ENV!=='production'){const value = el.attrsMap['v-bind:value']|| el.attrsMap[':value']const typeBinding = el.attrsMap['v-bind:type']|| el.attrsMap[':type']if(value &&!typeBinding){const binding = el.attrsMap['v-bind:value']?'v-bind:value':':value'warn(`${binding}="${value}" conflicts with v-model on the same element `+'because the latter already expands to a value binding internally', el.rawAttrsMap[binding])}}// Modifier handling修饰符处理
const{ lazy, number, trim }= modifiers ||{}const needCompositionGuard =!lazy && type !=='range'const event = lazy
?'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})`)addHandler(el, event, code,null,true)if(trim || number){addHandler(el,'blur','$forceUpdate()')}// dev/src/compiler/directives/model.js line 36exportfunctiongenAssignmentCode(value: string,assignment: string): string {const res =parseModel(value)if(res.key ===null){return`${value}=${assignment}`}else{return`$set(${res.exp}, ${res.key}, ${assignment})`}}