Vue源码解析18-自定义组件的v-model

这篇文章主要讨论自定义组件的v-model处理,会顺带提一提普通标签上的v-model处理。v-model算是比较复杂的一个内置指令了,主要是对于不同的宿主元素它需要不同的特殊处理,所以分支比较多。

parse

parse阶段,它是和自定义指令在processAttrs函数内的同一个分支进行处理的,不同的是会多一个校验:

1
2
3
4
5
6
7
8
function processAttrs(el) {
// ...
addDirective(el, name, rawName, value, arg, modifiers);
if (process.env.NODE_ENV !== 'production' && name === 'model') {
checkForAliasModel(el, value);
}
// ...
}

checkForAliasModel用于检查v-model的参数是否是v-for的迭代对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function checkForAliasModel(el, value) {
let _el = el;
while (_el) {
if (_el.for && _el.alias === value) {
warn(
`<${el.tag} v-model="${value}">: ` +
`You are binding v-model directly to a v-for iteration alias. ` +
`This will not be able to modify the v-for source array because ` +
`writing to the alias is like modifying a function local variable. ` +
`Consider using an array of objects and use v-model on an object property instead.`,
);
}
_el = _el.parent;
}
}

这个函数会去找是否有某个祖先元素存在v-for,而不仅仅是父元素。

generate

同样和指令处理流程一致,会调用genDirectives

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
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;
// state.directives目前包含v-on、v-bind、v-cloak、v-html、v-model、v-text
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) + ']';
}
}

v-model指令存在于state.directives之中,所以gen会被执行,v-modelgen位于src/platforms/web/compiler/directives/model.jsmodel函数:

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
export default function model(el: ASTElement, dir: ASTDirective, _warn: Function): ?boolean {
warn = _warn;
const value = dir.value;
const modifiers = dir.modifiers;
const tag = el.tag;
const 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(`<${el.tag} v-model="${value}" type="file">:\n` + `File inputs are read only. Use a v-on:change listener instead.`);
}
}

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(
`<${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.',
);
}

// ensure runtime directive metadata
return true;
}

这里针对自定义组件、selectcheckboxradiotextarea做了特殊处理,我们的兴趣在自定义组件,其余的可以自己去了解,代码并不复杂。

可以看到针对自定义组件,model函数返回的是false,所以在genDirectives中也不会把结果放到res中,之后在patch阶段也不会执行各种指令钩子函数。接下来看看genComponentModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Cross-platform code generation for component v-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);

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

光看这段代码有点抽象,况且还调用了其他子函数,最好使用测试代码打个断点看看。我们使用的测试代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="app"><my-comp v-model="txt"></my-comp></div>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data() {
return {
txt: 'ttttt',
};
},
components: {
myComp: {
props: {
value: {
type: String,
default: '',
},
},
template: `
<p>{{value}}</p>
`,
},
},
});
</script>

执行完genComponentModel后,el.model添加的 3 个属性值为:

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

genAssignmentCode子函数主要是处理绑定到v-model的各种形式,如value、value.a、value['a']、value[0],这里不赘述。

之后在genData中又会有专门的分支处理el.model,这是为自定义组件准备的:

1
2
3
4
5
6
7
8
export function genData(el: ASTElement, state: CodegenState): string {
// ...
// component v-model
if (el.model) {
data += `model:{value:${el.model.value},callback:${el.model.callback},expression:${el.model.expression}},`;
}
//...
}

所以我们的测试代码最后给data对象添加的属性值为:

1
2
3
4
5
6
7
8
9
`
data.model = {
value: txt,
callback: function($$v) {
txt = $$v;
},
expression: 'txt',
};
`;

render 生成 vnode

自定义组件在创建vnode对象时,会调用createComponent方法(位于src/core/vdom/create-component.js),其中会专门处理data.model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}

//...
}

因为自定义组件可以定制v-modelpropsevent名称,transformModel就是来处理这种定制情形的:

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

在拿到propevent的真正名称后,就会将data.model.valuedata.model.callback赋值给data.propsdata.on,之后v-model的痕迹就消失了,到这里应该可以看出来v-model其实只是一个语法糖。