Vuejs源码解析 6-2 v-model

在理解 Vue 的时候都把 Vue 的数据响应原理理解为双向绑定,但实际上这是不准确的,我们之前提到的数据响应,都是通过数据的改变去驱动 DOM 视图的变化,而双向绑定除了数据驱动 DOM 外, DOM 的变化反过来影响数据,是一个双向关系,在 Vue 中,我们可以通过 v-model 来实现双向绑定。

v-model 即可以作用在普通表单元素上,又可以作用在组件上,它其实是一个语法糖,接下来我们就来分析 v-model 的实现原理。

作用在表单元素

为了更加直观,我们还是结合示例来分析:

1
2
3
4
5
6
7
8
9
10
11
12
let vm = new Vue({
el: '#app',
template: '<div>'
+ '<input v-model="message" placeholder="edit me">' +
'<p>Message is: {{ message }}</p>' +
'</div>',
data() {
return {
message: ''
}
}
})

我们在 input 元素上设置了 v-model 属性,绑定了 message,当我们在 input 上输入了内容,message 也会同步变化。接下来我们就来分析 是如何实现这一效果的,其实非常简单。
也是先从编译阶段分析,首先是 parse 阶段, v-model 被当做普通的指令解析到 el.directives

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
function processAttrs(el) {
var list = el.attrsList;
var 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)) {
// mark element as dynamic
el.hasBindings = true;
// modifiers
modifiers = parseModifiers(name.replace(dirRE, ''));
// support .foo shorthand syntax for the .prop modifier
if (modifiers) {
name = name.replace(modifierRE, '');
}
if (bindRE.test(name)) { // v-bind
name = name.replace(bindRE, '');
value = parseFilters(value);
isDynamic = dynamicArgRE.test(name);
if (isDynamic) {
name = name.slice(1, -1);
}
if (
process.env.NODE_ENV !== 'production' &&
value.trim().length === 0
) {
warn$2(
("The value for a v-bind expression cannot be empty. Found in \"v-bind:" + name + "\"")
);
}
if (modifiers) {
if (modifiers.prop && !isDynamic) {
name = camelize(name);
if (name === 'innerHtml') { name = 'innerHTML'; }
}
if (modifiers.camel && !isDynamic) {
name = camelize(name);
}
if (modifiers.sync) {
syncGen = genAssignmentCode(value, "$event");
if (!isDynamic) {
addHandler(
el,
("update:" + (camelize(name))),
syncGen,
null,
false,
warn$2,
list[i]
);
if (hyphenate(name) !== camelize(name)) {
addHandler(
el,
("update:" + (hyphenate(name))),
syncGen,
null,
false,
warn$2,
list[i]
);
}
} else {
// handler w/ dynamic event name
addHandler(
el,
("\"update:\"+(" + name + ")"),
syncGen,
null,
false,
warn$2,
list[i],
true // dynamic
);
}
}
}
if ((modifiers && modifiers.prop) || (
!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
)) {
addProp(el, name, value, list[i], isDynamic);
} else {
addAttr(el, name, value, list[i], isDynamic);
}
} else if (onRE.test(name)) { // v-on
name = name.replace(onRE, '');
isDynamic = dynamicArgRE.test(name);
if (isDynamic) {
name = name.slice(1, -1);
}
addHandler(el, name, value, modifiers, false, warn$2, list[i], isDynamic);
} else { // normal directives
name = name.replace(dirRE, '');
// parse arg
var argMatch = name.match(argRE);
var 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 {
// ...
}
}
}

关注 addDirective 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function addDirective(
el,
name,
rawName,
value,
arg,
isDynamicArg,
modifiers,
range
) {
(el.directives || (el.directives = [])).push(rangeSetItem({
name: name,
rawName: rawName,
value: value,
arg: arg,
isDynamicArg: isDynamicArg,
modifiers: modifiers
}, range));
el.plain = false;
}

然后在 codegen 阶段,执行 genData 的时候,会执行 const dirs = genDirectives(el, state),它的定义在 src/compiler/codegen/index.js 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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
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.arg}"` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}
}
if (hasRuntime) {
return res.slice(0, -1) + ']'
}
}

genDirectives 方法就是遍历 el.directives,然后获取每一个指令对应的方法 const gen: DirectiveFunction = state.directives[dir.name],这个指令方法实际上是在实例化 CodegenState 的时候通过 option 传入的,这个 option 就是编译相关的配置,它在不同的平台下配置不同,在 web 环境下的定义在 src/platforms/web/compiler/options.js 下:

1
2
3
4
5
6
7
8
9
10
11
12
export const baseOptions: CompilerOptions = {
expectHTML: true,
modules,
directives,
isPreTag,
isUnaryTag,
mustUseProp,
canBeLeftOpenTag,
isReservedTag,
getTagNamespace,
staticKeys: genStaticKeys(modules)
}

directives 定义在 src/platforms/web/compiler/directives/index.js 中:

1
2
3
4
5
export default {
model,
text,
html
}

对于 v-model 而言,对应的 directive函数 是在 src/platforms/web/compiler/directives/model.js 中定义的 model 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function model(
el,
dir,
_warn
) {
warn$1 = _warn;
var value = dir.value;
var modifiers = dir.modifiers;
var tag = el.tag;
var type = el.attrsMap.type;

if (process.env.NODE_ENV !== 'production') {
// inputs with type="file" are read only and setting the input's
// value will throw an error.
if (tag === 'input' && type === 'file') {
warn$1(
"<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" +
"File inputs are read only. Use a v-on:change listener instead.",
el.rawAttrsMap['v-model']
);
}
}

if (el.component) {
genComponentModel(el, value, modifiers);
// component v-model doesn't need extra runtime
return false
} else if (tag === 'select') {
genSelect(el, value, modifiers);
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers);
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers);
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers);
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers);
// component v-model doesn't need extra runtime
return false
} else if (process.env.NODE_ENV !== 'production') {
warn$1(
"<" + (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 metadata
return true
}

也就是说我们执行 needRuntime = !!gen(el, dir, state.warn) 就是在执行 model函数

它会根据 AST 元素节点的不同情况去执行不同的逻辑,对于我们这个 case 而言,它会命中 genDefaultModel(el, value, modifiers) 的逻辑。
稍后我们也会介绍组件的处理。我们来看一下 genDefaultModel 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function genDefaultModel(
el,
value,
modifiers
) {
var type = el.attrsMap.type;

// ...

var ref = modifiers || {};
var lazy = ref.lazy;
var number = ref.number;
var trim = ref.trim;
var needCompositionGuard = !lazy && type !== 'range';
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 + ")";
}

var 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()');
}
}

genDefaultModel 函数先处理了 modifiers,它的不同主要影响的是 eventvalueExpression 的值

对于我们的例子,eventinputvalueExpression$event.target.value。然后去执行 genAssignmentCode 去生成代码,它的定义在 src/compiler/directives/model.js 中:

1
2
3
4
5
6
7
8
9
10
11
function genAssignmentCode(
value,
assignment
) {
var res = parseModel(value);
if (res.key === null) {
return (value + "=" + assignment)
} else {
return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")")
}
}

该方法首先对 v-model 对应的 value 做了解析,它处理了非常多的情况,对我们的例子,value 就是 messgae,所以返回的 res.keynull,然后我们就得到 ${value}=${assignment},也就是 message=$event.target.value

然后我们又命中了 needCompositionGuardtrue 的逻辑,所以最终的 code 为:if($event.target.composing)return;message=$event.target.value

code 生成完后,又执行了 2 句非常关键的代码:

1
2
addProp(el, 'value', `(${value})`)
addHandler(el, event, code, null, true)

这实际上就是 input 实现 v-model 的精髓
通过修改 AST 元素,给 el 添加一个 prop,相当于我们在 input 上动态绑定了 value,又给 el 添加了事件处理,相当于在 input 上绑定了 input 事件,其实转换成模板如下:

1
2
3
<input
:value="message"
@input="message=$event.target.value">

其实就是动态绑定了 inputvalue 指向了 messgae 变量,并且在触发 input事件的时候去动态把 message 设置为目标值,这样实际上就完成了数据双向绑定了,所以说 v-model 实际上就是语法糖

再回到 genDirectives,它接下来的逻辑就是根据指令生成一些 data 的代码:

1
2
3
4
5
6
7
8
9
10
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)}` : ''
}},`
}

对我们的例子而言,最终生成的 render 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
with(this) {
return _c(
'div',
[
_c('input',
{
directives:[{
name:"model",
rawName:"v-model",
value:(message),
expression:"message"
}],
attrs:{"placeholder":"edit me"},
domProps:{"value":(message)},
on:{"input":function($event){
if($event.target.composing)
return;
message=$event.target.value
}}
}
),
_c('p',[_v("Message is: "+_s(message))])
]
)
}

关于事件的处理我们之前的章节已经分析过了,所以对于 inputv-model 而言,完全就是语法糖,并且对于其它表单元素套路都是一样,区别在于生成的事件代码会略有不同。

v-model 除了作用在表单元素上,新版的 Vue 还把这一语法糖用在了组件上,接下来我们来分析它的实现。

总结:

  1. 编译:parse: v-model -> el.directives
  2. 编译:codegen: genData —> const dirs = genDirectives(el, state)
  3. needRuntime = !!gen(el, dir, state.warn) 执行model函数
  4. genDefaultModel(el, value, modifiers)
  5. genAssignmentCode:对 value 做解析
  6. addProp,addHandler
  7. <input v-bind:value="message" v-on:input="message=$event.target.value">
  8. render代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    with(this) {
    return _c('div',[_c('input',{
    directives:[{
    name:"model",
    rawName:"v-model",
    value:(message),
    expression:"message"
    }],
    attrs:{"placeholder":"edit me"},
    domProps:{"value":(message)},
    on:{"input":function($event){
    if($event.target.composing)
    return;
    message=$event.target.value
    }}}),_c('p',[_v("Message is: "+_s(message))])
    ])
    }

v-model的额外处理

然而 v-model 仅仅是起到合并语法,创建一个新的语法糖的意义吗? 显然答案是否定的,对于需要使用输入法 (如中文、日文、韩文等) 的语言,你会发现 v-model 不会在输入法组合文字过程中得到更新。这就是 v-model 的一个重要的特点。它会在事件处理这一层添加新的事件监听 compositionstartcompositionend,他们会分别在语言输入的开始和结束时监听到变化,只要借助$event.target.composing,就可以设计出只会在输入法组合文字的结束阶段才更新数据,这有利于提高用户的使用体验。

作用在组件:

为了更加直观,我们也是通过一个例子分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let Child = {
template: '<div>'
+ '<input :value="value" @input="updateValue" placeholder="edit me">' +
'</div>',
props: ['value'],
methods: {
updateValue(e) {
this.$emit('input', e.target.value)
}
}
}

let vm = new Vue({
el: '#app',
template: '<div>' +
'<child v-model="message"></child>' +
'<p>Message is: {{ message }}</p>' +
'</div>',
data() {
return {
message: ''
}
},
components: {
Child
}
})

可以看到,父组件引用 child 子组件的地方使用了 v-model 关联了数据 message
而子组件定义了一个 valueprop,并且在 input 事件的回调函数中,通过 this.$emit('input', e.target.value) 派发了一个事件,为了让 v-model 生效,这两点是必须的

接着我们从源码角度分析实现原理,还是从编译阶段说起,对于父组件而言,在编译阶段会解析 v-model 指令,依然会执行 genData 函数中的 genDirectives 函数,接着执行 src/platforms/web/compiler/directives/model.js 中定义的 model 函数,并命中如下逻辑:

1
2
3
4
else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers);
return false
}

genComponentModel 函数定义在 src/compiler/directives/model.js 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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)

el.model = {
value: `(${value})`,
expression: `"${value}"`,
callback: `function (${baseValueExpression}) {${assignment}}`
}
}

genComponentModel 的逻辑很简单,对我们的例子而言,生成的 el.model 的值为:

1
2
3
4
5
el.model = {
callback:'function ($$v) {message=$$v}',
expression:'"message"',
value:'(message)'
}

那么在 genDirectives 之后,genData 函数中有一段逻辑如下:

1
2
3
4
5
6
7
8
9
if (el.model) {
data += `model:{value:${
el.model.value
},callback:${
el.model.callback
},expression:${
el.model.expression
}},`
}

那么父组件最终生成的 render 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
with(this){
return _c('div',[_c('child',{
model:{
value:(message),
callback:function ($$v) {
message=$$v
},
expression:"message"
}
}),
_c('p',[_v("Message is: "+_s(message))])],1)
}

然后在创建子组件 vnode 阶段,会执行 createComponent 函数,它的定义在 src/core/vdom/create-component.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// ...
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}

// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// ...
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// ...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)

return vnode
}

其中会对 data.model 的情况做处理,执行 transformModel(Ctor.options, data) 方法:

1
2
3
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}

其中会对 data.model 的情况做处理,执行 transformModel(Ctor.options, data) 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
;(data.props || (data.props = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
if (isDef(on[event])) {
on[event] = [data.model.callback].concat(on[event])
} else {
on[event] = data.model.callback
}
}

transformModel 逻辑很简单,给 data.props 添加 data.model.value,并且给 data.on 添加 data.model.callback,对我们的例子而言,扩展结果如下:

1
2
3
4
5
6
7
8
data.props = {
value: (message),
}
data.on = {
input: function ($$v) {
message=$$v
}
}

其实就相当于我们在这样编写父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let vm = new Vue({
el: '#app',
template: '<div>' +
'<child :value="message" @input="message=arguments[0]"></child>' +
'<p>Message is: {{ message }}</p>' +
'</div>',
data() {
return {
message: ''
}
},
components: {
Child
}
})

子组件传递的 value 绑定到当前父组件的 message,同时监听自定义 input 事件,当子组件派发 input 事件的时候,父组件会在事件回调函数中修改 message 的值,同时 value 也会发生变化,子组件的 input 值被更新。

这就是典型的 Vue 的父子组件通讯模式,父组件通过 prop 把数据传递到子组件,子组件修改了数据后把改变通过 $emit 事件的方式通知父组件,所以说组件上的 v-model 也是一种语法糖。

另外我们注意到组件 v-model 的实现,子组件的 value prop 以及派发的 input 事件名是可配的,可以看到 transformModel 中对这部分的处理:

1
2
3
4
5
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
// ...
}

也就是说可以在定义子组件的时候通过 model 选项配置子组件接收的 prop 名以及派发的事件名,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let Child = {
template: '<div>'
+ '<input :value="msg" @input="updateValue" placeholder="edit me">' +
'</div>',
props: ['msg'],
model: {
prop: 'msg',
event: 'change'
},
methods: {
updateValue(e) {
this.$emit('change', e.target.value)
}
}
}

let vm = new Vue({
el: '#app',
template: '<div>' +
'<child v-model="message"></child>' +
'<p>Message is: {{ message }}</p>' +
'</div>',
data() {
return {
message: ''
}
},
components: {
Child
}
})

子组件修改了接收的 prop 名以及派发的事件名,然而这一切父组件作为调用方是不用关心的,这样做的好处是我们可以把 value 这个 prop 作为其它的用途。

1
2
3
4
5
6
props: ['value'],
methods: {
updateValue(e) {
this.$emit('input', e.target.value)
}
}

vs

1
2
3
4
5
6
7
8
9
10
props: ['msg'],
model: {
prop: 'msg',
event: 'change'
}
methods: {
updateValue(e) {
this.$emit('change', e.target.value)
}
}

总结

  1. 编译:parse: v-model -> el.directives
  2. 编译:codegen: genData —> const dirs = genDirectives(el, state)
  3. needRuntime = !!gen(el, dir, state.warn) 执行model函数
  4. genComponentModel(el, value, modifiers)

总结:

至此,v-model 的实现就分析完了,我们了解到它是 Vue 双向绑定的真正实现,但本质上就是一种语法糖,它即可以支持原生表单元素,也可以支持自定义组件
在组件的实现中,我们是可以配置子组件接收的 prop 名称,以及派发的事件名称