[Vue 源码] v-model 逻辑分析

v-model

v-model 和前面分析过的 v-on 一样,都是 Vue 提供的指令,所以 v-model 的分析流程和 v-on 相似。围绕模板的编译、 render 函数的生成,到最后的真实节点的挂载。 v-model 无论什么使用场景,本质上都是一个语法糖。

基础使用

v-model 和表单脱离不了关系,之所以视图能影响数据,本质上这个视图是可交互的,因此表单是实现这一交互的前提。表单的使用以 <input> <textarea> <select> 为核心,来看下具体的使用方式

// 普通输入框
<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> 

先来回顾一下模版到真实节点的过程。

  • 1.模版解析成 AST
  • 2.AST 树生成可执行的 render 函数的生成
  • 3.render 函数转换成虚拟 DOM 对象
  • 4.根据虚拟 DOM 对象生成真实 DOM 节点

模版解析

通过前面的分析已经知道,模版编译阶段,会调用 const ast = parse(template.trim(), options) 生成 AST 树, 而对于 v-model 的处理, 集中在 processAttrs 函数上。

processAttrs 的处理过程中,对模版的属性处理分成两部分,一部分是对普通 html 标签属性的处理,一部分是对 vue 指令的处理。而对于 vue 指令的处理中,又对 v-on v-bind 进行了特殊的处理,其他的 Vue 指令都会执行 addDirective 过程进行处理。

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 标签属性处理过程}}
} 

在对事件机制的分析过程中,我们知道, Vuev-on 指令的处理是为 AST 树添加 events 属性,类似的,普通指令会在 AST 树上添加 directives 属性。

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
} 

最终 AST 树上多了一个 directives 属性,如下图所示,其中 modifiers 代表模版中添加的修饰符,如 .lazy .number

AST 树中的 directives 属性

render 函数的生成

render 函数的生成阶段,也就是前面分析过的 genData 逻辑,其中 genData 会对模版的诸多属性进行处理,并返回最终拼接好的字符串模版,而对指令的处理会进入 genDirectives 流程

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

genDirectives 的逻辑并不复杂, 通过遍历 directives 数组,最终以 directives:[ 包裹的字符串返回

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) + ']'}
} 

genDirectives 函数中,会通过 state.directives[dir.name] 拿到对应指令的处理函数,而这些指令的处理函数针对不同的平台又有不同的实现。在编译过程中,通过偏函数的方式,分离了不同平台的不同编译过程,也为每一个平台每次提供相同的配置进行了选项合并,并进行了缓存。针对浏览器而言,有三个重要的指令选项

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

state.directives[dir.name] 也就是对应的 model 函数,来看下 model 函数的逻辑

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
} 

显然,在 model 函数中会对 AST 树做进一步处理,我们知道表单有不同的类型,不同类型对应的事件响应机制也不同。因此需要针对不同的表单控件生成不同的 render 函数,这里我们重点分析 input 标签的处理, 也就是 getDefaultModel 方法。

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 + ")")}
} 

getDefaultModel 的逻辑分为两部分,一部分是针对修饰符产生不同的事件处理字符串,而是为 v-model 产生的 AST 树添加属性和事件相关的属性,关键的两行代码就是

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

回到 genData , 通过 genDirectives 处理之后,原先的 AST 新增了两个属性,因此在字符串处理过程中同样需要处理 propsevents 的分支, 最终 render 函数的结果为

"_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}}})"

生成真实 DOM

在生成真实 DOM 之前需要先生成虚拟 DOM , 生成虚拟 DOM 的过程和之前相同,没有特别的地方。有了虚拟 DOM 之后,就开始生成真实 DOM , 也就是 patchVnode ,其中关键是 createElm 方法,在前面的到的指令相关的信息会保存在 vnodedata 属性中,所以所属性的处理会走 invokeCreateHooks 逻辑

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

invokeCreateHooks 会调用定义好的钩子函数,对 vnode 上定义的属性、指令、事件等进行真实 DOM 的处理,包括一下步骤(部分)

  • 1.updateDOMProps 会利用 vnode data 上的 domProps 更新 input 标签的 value
  • 2.updateAttrs 会利用 vnode data 上的 attrs 属性更新节点的属性值
  • 3.updateDOMListeners 利用 vnode data 上的 on 属性添加事件监听

因此 v-model 语法糖最终反应的结果,是通过监听表单控件自身的 input 事件(其他类型有不同的监听事件类型),去影响自身的 value 值。

组件使用 v-model

组件上使用 v-model 本质上是父子组件通信的语法糖。 先来看一个简单的例子

 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'
 }) 

父组件上使用 v-model ,子组件默认会利用名为 value 的 prop 和名为 input 的事件, AST 生成阶段和普通表单控件的区别在于,当遇到 child 是,由于不是普通的 html 标签,会执行 getComponentModel 的过程,而 getComponentModel 的结果在 AST 树上添加 model 属性。

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}}`}
} 

经过对 AST 树的处理后,回到 genData 的流程,由于又了 model 属性,父组件拼接的字符串会做进一步的处理。

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}",`}
} 

因此,父组件最终的 render 函数表现为

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

子组件的创建阶段赵丽会执行 createComponent , 其中对 model 的逻辑需要特别说明

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;}
} 

从 transformModel 的逻辑可以看出, 子组件的 vnode 会为 data.props 添加 data.model.value , 并且给 data.on 添加 data.model.callback 。


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

从 transformModel 的逻辑可以看出, 子组件的 vnode 会为 data.props 添加 data.model.value , 并且给 data.on 添加 data.model.callback 。

显然,这种写法就是时间通信的写法,这个过程有回到了对事件指令的分析过程。在组件上使用 v-mode 本质上还是一个父子组件通信的语法糖。

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

猜你喜欢

转载自blog.csdn.net/qq_53225741/article/details/129364265