前言
今天介绍Vue.js中一种常用的指令:v-model,以及v-model指令特性带来的一些使用。
具体分析
依旧是以简单实例为引去分析Vue.js中针对v-model的处理,具体实例:
<input v-model="text" />
<script>
new Vue({
data: {
text: '测试v-model'
}
})
</script>
实际上v-model的处理还需要从render函数的构建来说,即baseCompile来完成ast树的构建。
parse解析v-model
实际上这部分构建v-model在ast结构中的具体结构,具体的处理逻辑如下:
Vue源码中对于v-model在静态解析阶段,实际上分为两部分:
- 解析出v-model部分
- 针对v-model指令的特殊处理
首先第一点:解析出v-model部分,此处的处理与Vue.js中标签属性是相同的处理,即v-model作为属性名,就实例而言,即{‘v-model’: text}
第二点:v-model指令特殊处理,v-model虽然是标签的属性,但是是Vue.js中的指令,此时就是上面逻辑中给出的,实际上最后会调用processAttrs来处理
processAttrs
下面就来看看针对v-model,这边相关的处理逻辑如下:
从上面的逻辑过程中可知:
processAttrs实际上处理三种特殊的属性:指令、事件绑定、prop
而针对指令的相关处理实际上主要就是调用:addDirective,该函数的作用就是构建指令对象并创建directives对象,即:
el.directives.push({
// 不带v-前缀的指令名
name,
// 完整名称
rawName,
// 指令的绑定值
value,
// 传递给指令的参数
arg,
// 一个包含修饰符的对象
modifiers
});
generate中处理v-model部分
generate函数实际上就是将parse出来的code解析成指定结构的AST。
就以实例来说,在parse阶段实际上v-model实际上构建成了如下的结构并保存在directives中:
{
arg: null
modifiers: undefined
name: "model"
rawName: "v-model"
value: "text"
}
而在generate这个阶段的处理,主要逻辑如下:
上图中的主要逻辑都是建立在实例基础上的,实际上genElement中还有其他的逻辑针对不同情况,但是不是本篇要讨论的。实际的处理实际上会调用genData$2函数,而genData$2中关键的处理是genDirectives。
genDirectives中会调用model函数,而该函数实际上是针对不同的标签来进行处理,主要的处理逻辑如下:
对于标签是input的v-model的处理,实际上会调用getDefaultModel函数,该函数就是处理v-model成特定的结构,实际上还部分的逻辑主要就两点:
将v-model绑定的值作为prop
定义相关的事件,即v-model
第一点:调用addProp函数
(el.props || (el.props = [])).push({ name: name, value: value });
第二点:定义事件,如果modifiers中定义了lazy就会定义change事件,否则定义input事件
// 定义el.events
var events = el.events || (el.events = {});
// 这里兼容了多个相同事件的定义,如果用一个事件存在多个实际上这里就会保存为数组
/*
这里的code实际上对于当前实例则是
if($event.target.composing) return;
text=$event.target.value
*/
events[name] = { value: code}
之后的流程处理就回到了genData$2中了,而上面逻辑的定义props和event则会在接下来的逻辑处理中进行相关处理。
// v-model中定义props会在这里处理
if (el.props) {
data += "domProps:{" + (genProps(el.props)) + "},";
}
// v-model定义events会在这里处理
if (el.events) {
data += (genHandlers(el.events, false, state.warn)) + ",";
}
经历过generate的处理,实际上构建出Vue.js指定的结构了,就以实例而言,v-model所在标签构建的结构如下:
_c('input',
{
directives:[
{
name:"model",
rawName:"v-model",
value:(text),
expression:"text"
}
],
domProps:{"value":(text)},
on:{
"input":function($event){
if($event.target.composing) return;
text=$event.target.value
}
}
}
)
补充
上面的处理是基于实例的基础的逻辑,而实际上model函数针对标签的类型有不同的处理逻辑,主要类型分为:
- 子组件
- select标签
- checkbox标签
- radio标签
- input或textarea标签
- 非HTML标签或SVG标签
子组件v-model
子组件会调用genComponentModel函数来处理,主要的处理逻辑是定义model属性,即:
el.model = {
value,
expression,
callback,
};
而在genData$2中处理如下:
// component v-model
if (el.model) {
data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
}
非html或SVG标签也是调用该函数进行相关处理。
select标签v-model
调用genSelect函数,定义change事件:
var number = modifiers && modifiers.number;
// 从这里可以看出v-model背后的主要处理了
var selectedVal = "Array.prototype.filter" +
".call($event.target.options,function(o){return o.selected})" +
".map(function(o){var val = \"_value\" in o ? o._value : o.value;" +
"return " + (number ? '_n(val)' : 'val') + "})";
var assignment = '$event.target.multiple ? $$selectedVal : $$selectedVal[0]';
var code = "var $$selectedVal = " + selectedVal + ";";
code = code + " " + (genAssignmentCode(value, assignment));
// 定义events属性
addHandler(el, 'change', code, null, true);
checkbox和radio标签v-model
这两种标签主要的处理逻辑基本相同(但是不是同一个函数),定义checked属性和change事件,主要的处理还是change中获取当前状态
v-model使用姿势
从上面整个分析,v-model在编辑过程的结构就比较清晰了,下面列举下场使用子组件更新prop内容的几种方式以及model修改:
-
在子组件中常使用input更新父组件中传递的prop:
function(e) { this.$emit('input', e); }
-
在子组件中修改v-model的默认事件,提供修改v-model的默认处理
model: { prop: 'checked', event: 'change' }, props: { value: String, checked: { type: Number, default: 0 } }
再看上面的一些使用,估计就很清晰了,实际上Vue.js也提供sync修饰符 + update事件来实现子组件更新prop值。