11.2 组件使用v-model
最后我们简单说说在父组件中使用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
的事件,当然像select
表单会以其他默认事件的形式存在。分析源码的过程也大致类似,这里只列举几个特别的地方。
AST
生成阶段和普通表单控件的区别在于,当遇到child
时,由于不是普通的html
标签,会执行getComponentModel
的过程,而getComponentModel
的结果是在AST
树上添加model
的属性。
function model() {
if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers);
}
}
function genComponentModel (el,value,modifiers) {
var ref = modifiers || {};
var number = ref.number;
var trim = ref.trim;
var baseValueExpression = '$$v';
var valueExpression = baseValueExpression;
if (trim) {
valueExpression =
"(typeof " + baseValueExpression + " === 'string'" +
"? " + baseValueExpression + ".trim()" +
": " + baseValueExpression + ")";
}
if (number) {
valueExpression = "_n(" + valueExpression + ")";
}
var assignment = genAssignmentCode(value, valueExpression);
// 在ast树上添加model属性,其中有value,expression,callback属性
el.model = {
value: ("(" + value + ")"),
expression: JSON.stringify(value),
callback: ("function (" + baseValueExpression + ") {" + assignment + "}")
};
}
最终AST
树的结果:
{
model: {
callback: "function ($$v) {message=$$v}"
expression: ""message""
value: "(message)"
}
}
经过对AST
树的处理后,回到genData$2
的流程,由于有了model
属性,父组件拼接的字符串会做进一步处理。
function genData$2 (el, state) {
var data = '{';
var dirs = genDirectives(el, state);
···
// v-model组件的render函数处理
if (el.model) {
data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
}
···
return data
}
因此,父组件最终的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-model
语法糖本质上可以修改为'<child :value="message" @input="function(e){message = e}"></child>'
显然,这种写法就是事件通信的写法,这个过程又回到对事件指令的分析过程了。因此我们可以很明显的意识到,组件使用v-model
本质上还是一个子父组件通信的语法糖。