[Vue source code] v-model logic analysis

v-model

v-modelv-onLike the previously analyzed , they are all Vueinstructions provided by , v-modelso the analysis process of is v-onsimilar to that of . Around the compilation of the template, renderthe generation of the function, and the mounting of the final real node. v-modelNo matter what the usage scenario is, it is essentially a syntactic sugar.

basic use

v-modelIt is inseparable from the form. The reason why the view can affect the data is that the view is essentially interactive, so the form is the prerequisite for this interaction. The use of the form takes <input> <textarea> <select>as the core, let’s look at the specific usage

// 普通输入框
<input type="text" v-model="value1">
// 多行文本框
<textarea v-model="value2" cols="30" rows="10"></textarea>
// 单选框
<div class="group"><input type="radio" value="one" v-model="value3"> one<input type="radio" value="two" v-model="value3"> two
</div> 

Let's first review the process from the template to the real node.

  • 1. The template is parsed into ASTa tree
  • 2. ASTTree generation executable renderfunction generation
  • 3. renderConvert functions to virtual DOMobjects
  • DOM4. Generate real DOMnodes based on virtual objects

template parsing

Through the previous analysis, we know that in the template compilation stage const ast = parse(template.trim(), options), ASTthe tree will be called to generate, and the processing v-modelof will focus processAttrson the function.

In the process processAttrsof processing, the attribute processing of the template is divided into two parts, one part is htmlthe processing of ordinary tag attributes, and the other part is the processing of vuethe instruction . As for the processing of vuethe instruction , v-on v-bindspecial processing is carried out for , and other Vueinstructions will execute addDirectivethe process for processing.

function processAttrs (el) {const list = el.attrsListlet i, l, name, rawName, value, modifiers, syncGen, isDynamicfor (i = 0, l = list.length; i < l; i++) {name = rawName = list[i].namevalue = list[i].valueif (dirRE.test(name)) {// 对 Vue 指令的处理// mark element as dynamicel.hasBindings = true// modifiersmodifiers = parseModifiers(name.replace(dirRE, ''))if (bindRE.test(name)) { // v-bind // v-bind 指令处理 过程} else if (onRE.test(name)) { // v-on// v-on 指令处理过程} else { // normal directives// 对于非 v-bind v-on 的 vue 指令处理过程name = name.replace(dirRE, '')// parse argconst argMatch = name.match(argRE)let arg = argMatch && argMatch[1]isDynamic = falseif (arg) {name = name.slice(0, -(arg.length + 1))if (dynamicArgRE.test(arg)) {arg = arg.slice(1, -1)isDynamic = true}}addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])if (process.env.NODE_ENV !== 'production' && name === 'model') {checkForAliasModel(el, value)}}} else {// literal attribute// 普通 html 标签属性处理过程}}
} 

In the process of analyzing the event mechanism, we know that the processing of the instructionVue is to add attributes . Similarly, ordinary instructions will add attributes .v-onASTeventsASTdirectives

export function addDirective (el: ASTElement,name: string,rawName: string,value: string,arg: ?string,isDynamicArg: boolean,modifiers: ?ASTModifiers,range?: Range
) {(el.directives || (el.directives = [])).push(rangeSetItem({name,rawName,value,arg,isDynamicArg,modifiers}, range))el.plain = false
} 

Finally AST, there is an additional directivesattribute on the tree, as shown in the figure below, where modifiersrepresents the modifier added in the template, such as.lazy .number

directives attribute in the AST tree

Generation of the render function

renderThe generation phase of the function, which is the previously analyzed genDatalogic , genDatawill process many attributes of the template and return the final spliced ​​string template, and the processing of the instruction will enter genDirectivesthe process

export function genData (el: ASTElement, state: CodegenState): string {let data = '{'const dirs = genDirectives(el, state)if (dirs) data += dirs + ','// ... 
} 

The logic of genDirectives is not complicated, by traversing the array of directives, and finally returning the directives:[wrapped string

function genDirectives (el: ASTElement, state: CodegenState): string | void {const dirs = el.directivesif (!dirs) return// 字符串拼接let res = 'directives:['let hasRuntime = falselet i, l, dir, needRuntimefor (i = 0, l = dirs.length; i < l; i++) {dir = dirs[i]needRuntime = true// 对 AST 树重新处理const gen: DirectiveFunction = state.directives[dir.name]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 = trueres += `{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) + ']'}
} 

In genDirectivesthe function , state.directives[dir.name]the processing function of the corresponding instruction will be obtained through , and the processing function of these instructions has different implementations for different platforms. In the compilation process, the different compilation processes of different platforms are separated by means of partial functions, and the same configuration is provided for each platform each time for option merging and caching. For browsers, there are three important command options

 var directives = {model: model$1,text: text,html: html
}; 

state.directives[dir.name]And is the corresponding modelfunction, look at the logic of modelthe function

function model$1 (el,dir,_warn
) {warn$2 = _warn;// 绑定的值var value = dir.value;var modifiers = dir.modifiers;var tag = el.tag;var type = el.attrsMap.type;{//如果 input 元素的 type 是 file , 如果还使用 v-model 进行双向绑定则会发出警告if (tag === 'input' && type === 'file') {warn$2("<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" +"File inputs are read only. Use a v-on:change listener instead.",el.rawAttrsMap['v-model']);}}// 组件上的 v-modelif (el.component) {genComponentModel(el, value, modifiers);// component v-model doesn't need extra runtimereturn false} else if (tag === 'select') {// select 表单genSelect(el, value, modifiers);} else if (tag === 'input' && type === 'checkbox') {// checkboxgenCheckboxModel(el, value, modifiers);} else if (tag === 'input' && type === 'radio') {// radiogenRadioModel(el, value, modifiers);} else if (tag === 'input' || tag === 'textarea') {// 普通的 inputgenDefaultModel(el, value, modifiers);} else {// 如果不是以上几种类型,则默认为组件上的双向绑定genComponentModel(el, value, modifiers);// component v-model doesn't need extra runtimereturn false}// ensure runtime directive metadatareturn true
} 

Obviously model, ASTthe tree will be further processed in the function, we know that there are different types of forms, and the event response mechanisms corresponding to different types are also different. Therefore, different renderfunctions . Here we focus on analyzing inputthe processing of tags, which is getDefaultModelthe method .

function genDefaultModel (el,value,modifiers
) {var type = el.attrsMap.type;// 如果 v-bind 和 v-model 的值相同,则抛出错误{var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value'];var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];if (value$1 && !typeBinding) {var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value';warn$2(binding + "=\"" + value$1 + "\" conflicts with v-model on the same element " +'because the latter already expands to a value binding internally',el.rawAttrsMap[binding]);}}// 拿到 v-model 的修饰符var ref = modifiers || {};var lazy = ref.lazy;var number = ref.number;var trim = ref.trim;var needCompositionGuard = !lazy && type !== 'range';// lazy 修饰将触发同步的事件,从 input 改为 changevar event = lazy? 'change': type === 'range'? RANGE_TOKEN: 'input';var valueExpression = '$event.target.value';if (trim) {// 过滤输入的首尾空格valueExpression = "$event.target.value.trim()";}if (number) {// 将输入值转换成数字类型valueExpression = "_n(" + valueExpression + ")";}// 处理 v-model 的格式,允许使用如下格式 v-model=“a.b” v-mode="a[b]"var code = genAssignmentCode(value, valueExpression);if (needCompositionGuard) {// 确保不会在输入发组合文字过程中得到更新code = "if($event.target.composing)return;" + code;}// 添加 valueaddProp(el, 'value', ("(" + value + ")"));// 绑定事件addHandler(el, event, code, null, true);if (trim || number) {addHandler(el, 'blur', '$forceUpdate()');}
}

function genAssignmentCode (value,assignment
) {// 处理 v-model 的格式v-model="a.b"v-model="a[b]"var res = parseModel(value);if (res.key === null) {return (value + "=" + assignment)} else {return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")")}
} 

The logic of getDefaultModel is divided into two parts. One part is to generate different event processing strings for modifiers, but v-modelto ASTadd attributes and event-related attributes to the generated tree. The key two lines of code are

// 添加 value
addProp(el, 'value', ("(" + value + ")"));
// 绑定事件
addHandler(el, event, code, null, true); 

Going back genData, after genDirectivesprocessing , the original ASThas added two new attributes, so it is also necessary to propsprocess eventsthe branches of and in the string processing process, and the final result of renderthe function is

"_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"type":"text"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}})"

Generate real DOM

DOMIt is necessary to generate virtual before generating real DOM, and the process DOMof is the same as before, there is no special place. DOMAfter having the virtual , start to generate the real DOM, that is patchVnode, the key is createElmthe method the information related to the previous instruction will be stored in the attributevnode of the , so the processing of the attribute will follow the logicdatainvokeCreateHooks

function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index
) {// ....if (isDef(data)) {invokeCreateHooks(vnode, insertedVnodeQueue)}
} 

invokeCreateHooksIt will call the defined hook function vnodeto actually DOMprocess the properties, instructions, events, etc. defined above, including the following steps (parts)

  • 1. willupdateDOMProps update the value of the tag with thevnode datadomPropsinputvalue
  • 2. WillupdateAttrs update the attribute value of the node using the attribute onvnode dataattrs
  • 3. UseupdateDOMListeners the property on to add event listenersvnode dataon

Therefore, the final result of v-modelsyntactic sugar is inputto affect its own valuevalue .

Components use v-model

Using v-model on components is essentially syntactic sugar for parent-child component communication. Let's start with a simple example

 var child = {template: '<div><input type="text" :value="value" @input="emitEvent">{
   
   {value}}</div>',methods: {emitEvent(e) {this.$emit('input', e.target.value)}},props: ['value']}
 new Vue({ data() { return { message: 'test' } }, components: { child }, template: '<div id="app"><child v-model="message"></child></div>', el: '#app'
 }) 

Using v-model on the parent component, the child component will use the prop named value and the event named input by default. The difference between the AST generation phase and the ordinary form control is that when a child is encountered, it is not an ordinary html tag, it will The procedure of getComponentModel is executed, and the result of getComponentModel adds the model attribute on the AST tree.

export function genComponentModel (el: ASTElement,value: string,modifiers: ?ASTModifiers
): ?boolean {const { number, trim } = modifiers || {}const baseValueExpression = '$$v'let valueExpression = baseValueExpressionif (trim) {valueExpression =`(typeof ${baseValueExpression} === 'string'` +`? ${baseValueExpression}.trim()` +`: ${baseValueExpression})`}if (number) {valueExpression = `_n(${valueExpression})`}const assignment = genAssignmentCode(value, valueExpression)// 在 AST 树上添加 model 属性,其中有 value 、 expression 、 callback 属性el.model = {value: `(${value})`,expression: JSON.stringify(value),callback: `function (${baseValueExpression}) {${assignment}}`}
} 

After processing the AST tree, return to the process of genData. Due to the addition of the model attribute, the string spliced ​​by the parent component will be further processed.

function genData (el: ASTElement, state: CodegenState): string {const dirs = genDirectives(el, state)if (dirs) data += dirs + ','// ...// v-model 组件的 render 函数处理if (el.component) {data += `tag:"${el.tag}",`}
} 

Therefore, the final render function of the parent component behaves as

"_c('child',{model:{value:(message),callback:function ($$v) {message=$$v},expression:"message"}})" 

In the creation phase of subcomponents, Zhao Li will execute createComponent, and the logic of the model needs to be specially explained

function createComponent() {// transform component v-model data into props & eventsif (isDef(data.model)) {// 处理父组件的v-model指令对象transformModel(Ctor.options, data);}
} 
function transformModel (options, data) {// prop默认取的是value,除非配置上有model的选项var prop = (options.model && options.model.prop) || 'value';// event默认取的是input,除非配置上有model的选项var event = (options.model && options.model.event) || 'input'// vnode上新增props的属性,值为value;(data.attrs || (data.attrs = {}))[prop] = data.model.value;// vnode上新增on属性,标记事件var on = data.on || (data.on = {});var existing = on[event];var 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;}
} 

As can be seen from the logic of transformModel, the vnode of the subcomponent will add data.model.value to data.props, and add data.model.callback to data.on.


k].concat(existing);}} else {on[event] = callback;}
} 

As can be seen from the logic of transformModel, the vnode of the subcomponent will add data.model.value to data.props, and add data.model.callback to data.on.

Obviously, this way of writing is the way of time communication, and this process goes back to the analysis process of event instructions. Using v-mode on components is essentially a syntactic sugar for parent-child component communication.

at last

Recently, I also sorted out a JavaScript and ES note, a total of 25 important knowledge points, and explained and analyzed each knowledge point. It can help you quickly master the relevant knowledge of JavaScript and ES, and improve work efficiency.



Friends in need, you can click the card below to receive and share for free

Guess you like

Origin blog.csdn.net/qq_53225741/article/details/129364265