v-model
和前面分析过的 v-on
一样,都是 Vue
提供的指令,所以 v-model
的分析流程和 v-on
相似。围绕模板的编译、 render
函数的生成,到最后的真实节点的挂载。 v-model
无论什么使用场景,本质上都是一个语法糖。
v-model
和表单脱离不了关系,之所以视图能影响数据,本质上这个视图是可交互的,因此表单是实现这一交互的前提。表单的使用以
为核心,来看下具体的使用方式
// 普通输入框
// 多行文本框
// 单选框
one two
先来回顾一下模版到真实节点的过程。
AST
树AST
树生成可执行的 render
函数的生成render
函数转换成虚拟 DOM
对象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 标签属性处理过程}}
}
在对事件机制的分析过程中,我们知道, Vue
对 v-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
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
新增了两个属性,因此在字符串处理过程中同样需要处理 props
和 events
的分支, 最终 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
, 也就是 patchVnode
,其中关键是 createElm
方法,在前面的到的指令相关的信息会保存在 vnode
的 data
属性中,所以所属性的处理会走 invokeCreateHooks
逻辑
function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index
) {// ....if (isDef(data)) {invokeCreateHooks(vnode, insertedVnodeQueue)}
}
invokeCreateHooks
会调用定义好的钩子函数,对 vnode
上定义的属性、指令、事件等进行真实 DOM
的处理,包括一下步骤(部分)
updateDOMProps
会利用 vnode data
上的 domProps
更新 input
标签的 value
值updateAttrs
会利用 vnode data
上的 attrs
属性更新节点的属性值updateDOMListeners
利用 vnode data
上的 on
属性添加事件监听因此 v-model
语法糖最终反应的结果,是通过监听表单控件自身的 input
事件(其他类型有不同的监听事件类型),去影响自身的 value
值。
组件上使用 v-model 本质上是父子组件通信的语法糖。 先来看一个简单的例子
var child = {template: '{{value}}',methods: {emitEvent(e) {this.$emit('input', e.target.value)}},props: ['value']}
new Vue({ data() { return { message: 'test' } }, components: { child }, template: ' ', 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的相关知识,提升工作效率。
有需要的小伙伴,可以点击下方卡片领取,无偿分享