[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>
先来回顾一下模版到真实节点的过程。
-
- 模版解析成
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.attrsList
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
// 对 Vue 指令的处理
// mark element as dynamic
el.hasBindings = true
// modifiers
modifiers = 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 arg
const argMatch = name.match(argRE)
let arg = argMatch && argMatch[1]
isDynamic = false
if (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 函数的生成
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.directives
if (!dirs) return
// 字符串拼接
let res = 'directives:['
let hasRuntime = false
let i, l, dir, needRuntime
for (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 = 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) + ']'
}
}
在 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-model
if (el.component) {
genComponentModel(el, value, modifiers);
// component v-model doesn't need extra runtime
return false
} else if (tag === 'select') {
// select 表单
genSelect(el, value, modifiers);
} else if (tag === 'input' && type === 'checkbox') {
// checkbox
genCheckboxModel(el, value, modifiers);
} else if (tag === 'input' && type === 'radio') {
// radio
genRadioModel(el, value, modifiers);
} else if (tag === 'input' || tag === 'textarea') {
// 普通的 input
genDefaultModel(el, value, modifiers);
} else {
// 如果不是以上几种类型,则默认为组件上的双向绑定
genComponentModel(el, value, modifiers);
// component v-model doesn't need extra runtime
return false
}
// ensure runtime directive metadata
return 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 改为 change
var 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;
}
// 添加 value
addProp(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
之后,就开始生成真实 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
组件上使用 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 = baseValueExpression
if (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 & events
if (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 。
显然,这种写法就是时间通信的写法,这个过程有回到了对事件指令的分析过程。在组件上使用 v-mode 本质上还是一个父子组件通信的语法糖。
转载自:https://juejin.cn/post/7203640843562106937