双向数据绑定这个概念或者大家并不生疏,视图影响数据,数据同样影响视图,两者间有双向依赖的关系。在响应式零碎构建的上,中,下篇我曾经对数据影响视图的原理具体论述分明了。而如何实现视图影响数据这一关联?这就是本节探讨的重点:指令
v-model
。
因为 v-model
和后面介绍的插槽,事件统一,都属于 vue 提供的指令,所以咱们对 v-model
的剖析形式和以往大同小异。剖析会围绕模板的编译,render
函数的生成,到最初实在节点的挂载程序执行。最终咱们仍然会失去一个论断,v-model 无论什么应用场景,实质上都是一个语法糖。
11.1 表单绑定
11.1.1 根底应用
v-model
和表单脱离不了关系,之所以视图能影响数据,实质上这个视图须要可交互的,因而表单是实现这一交互的前提。表单的应用以 <input > <textarea> <select>
为外围,更细的划分联合 v-model
的应用如下:
// 一般输入框
<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>
// 原生单选框的写法 注:原生单选框的写法须要通过 name 绑定一组单选,两个 radio 的 name 属性雷同,能力体现为互斥
<div class="group">
<input type="radio" name="number" value="one">one
<input type="radio" name="number" value="two">two
</div>
// 多选框 (原始值:value4: [])
<div class="group">
<input type="checkbox" value="jack" v-model="value4">jack
<input type="checkbox" value="lili" v-model="value4">lili
</div>
// 下拉选项
<select name=""id="" v-model="value5">
<option value="apple">apple</option>
<option value="banana">banana</option>
<option value="bear">bear</option>
</select>
接下来的剖析,咱们以一般输入框为例
<div id="app">
<input type="text" v-model="value1">
</div>
new Vue({
el: '#app',
data() {
return {value1: ''}
}
})
进入注释前先回顾一下模板到实在节点的过程。
-
- 模板解析成
AST
树;
- 模板解析成
-
AST
树生成可执行的render
函数;
-
render
函数转换为Vnode
对象;
-
- 依据
Vnode
对象生成实在的Dom
节点。
- 依据
接下来,咱们先看看模板解析为 AST
树的过程。
11.1.2 AST 树的解析
模板的编译阶段,会调用 var ast = parse(template.trim(), options)
生成 AST
树,parse
函数的其余细节这里不开展剖析,后面的文章或多或少都波及过,咱们还是把关注点放在模板属性上的解析,也就是 processAttrs
函数上。
应用过 vue
写模板的都晓得,vue
模板属性由两局部组成,一部分是指令,另一部分是一般 html
标签属性。z 这也是属性解决的两大分支。而在指令的细分畛域,又将 v-on,v-bind
做非凡的解决,其余的一般分支会执行 addDirective
过程。
// 解决模板属性
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; // v-on:click
value = list[i].value; // doThis
if (dirRE.test(name)) { // 1. 针对指令的属性解决
···
if (bindRE.test(name)) { // v-bind 分支
···
} else if(onRE.test(name)) { // v-on 分支
···
} else { // 除了 v -bind,v-on 之外的一般指令
···
// 一般指令会在 AST 树上增加 directives 属性
addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i]);
if (name === 'model') {checkForAliasModel(el, value);
}
}
} else {// 2. 一般 html 标签属性}
}
}
在深刻分析 Vue 源码 – 揭秘 Vue 的事件机制这一节,咱们介绍了 AST
产生阶段对事件指令 v-on
的解决是为 AST
树增加 events
属性。相似的,一般指令会在 AST
树上增加 directives
属性,具体看 addDirective
函数。
// 增加 directives 属性
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;
}
最终 AST
树多了一个属性对象,其中 modifiers
代表模板中增加的修饰符,如:.lazy, .number, .trim
。
// AST
{
directives: {
{
rawName: 'v-model',
value: 'value',
name: 'v-model',
modifiers: undefined
}
}
}
11.1.3 render 函数生成
render
函数生成阶段,也就是后面剖析了数次的 generate
逻辑,其中 genData
会对模板的诸多属性进行解决, 最终返回拼接好的字符串模板,而对指令的解决会进入 genDirectives
流程。
function genData(el, state) {
var data = '{';
// 指令的解决
var dirs = genDirectives(el, state);
··· // 其余属性,指令的解决
// 针对组件的 v -model 解决,放到前面剖析
if (el.model) {data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
}
return data
}
genDirectives
逻辑并不简单, 他会拿到之前 AST
树中保留的 directives
对象,并遍历解析指令对象,最终以 'directives:['
包裹的字符串返回。
参考 Vue3 源码视频解说:进入学习
// directives render 字符串的生成
function genDirectives (el, state) {
// 拿到指令对象
var dirs = el.directives;
if (!dirs) {return}
// 字符串拼接
var res = 'directives:[';
var hasRuntime = false;
var i, l, dir, needRuntime;
for (i = 0, l = dirs.length; i < l; i++) {dir = dirs[i];
needRuntime = true;
// 对指令 ast 树的重新处理
var gen = 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) + ']'
}
}
这里有一句要害的代码 var gen = state.directives[dir.name]
, 为了理解其前因后果,咱们回到 Vue 源码中的编译流程,在以往的文章中,咱们残缺的介绍过template
模板的编译流程, 这一部分的设计是非常复杂且奇妙的,其中大量使用了偏函数的思维,即拆散了不同平台不同的编译过程,也为同一个平台每次提供雷同的配置选项进行了合并解决,并很好的将配置进行了缓存。其中针对浏览器端有三个重要的指令选项。
var directive$1 = {
model: model,
text: text,
html, html
}
var baseOptions = {
···
// 指令选项
directives: directives$1,
};
// 编译时传入选项配置
createCompiler(baseOptions)
而这个 state.directives['model']
也就是对应的 model
函数,所以咱们先把焦点聚焦在 model
函数的逻辑。
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;
{
// 这里遇到 type 是 file 的 html,如果还应用双向绑定会报出正告。// 因为 File inputs 是只读的
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']
);
}
}
// 组件上 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,如 text, 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 {
// 如果不是表单应用 v -model,同样会报出正告,双向绑定只针对表单控件。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
}
显然,model
会对表单控件的 AST
树做进一步的解决 ,在下面的根底用法中,咱们晓得 表单有不同的类型,每种类型对应的事件处理响应机制也不同 。因而咱们须要针对不同的表单控件生成不同的render
函数,因而须要产生不同的 AST
属性。model
针对不同类型的表单控件有不同的解决分支。咱们重点剖析一般 input
标签的解决,genDefaultModel
分支,其余类型的分支,能够仿照上面的剖析过程。
function genDefaultModel (el,value,modifiers) {
var type = el.attrsMap.type;
// v-model 和 v -bind 值雷同值,有抵触会报错
{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$1(
binding + "=\"" + value$1 + "\" conflicts with v-model on the same element "+'because the latter already expands to a value binding internally',
el.rawAttrsMap[binding]
);
}
}
// modifiers 存贮的是 v -model 的修饰符。var ref = modifiers || {};
// lazy,trim,number 是可供 v -model 应用的修饰符
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 + ")";
}
// genAssignmentCode 函数是为了解决 v -model 的格局,容许应用以下的模式:v-model="a.b" v-model="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 + ")")
}
}
genDefaultModel
的逻辑有两局部,一部分是针对修饰符产生不同的事件处理字符串,二是为 v-model
产生的 AST
树增加属性和事件相干的属性。其中最重要的两行代码是
// 增加 value 属性
addProp(el, 'value', ("(" + value + ")"));
// 绑定事件属性
addHandler(el, event, code, null, true);
addHandler
在之前介绍事件时剖析过,他会为 AST
树增加事件相干的属性, 同样的 addProp
也会为 AST
树增加 props
属性。最终 AST
树新增了两个属性:
回到 genData
, 通过genDirectives
解决后,原先的 AST
树新增了两个属性,因而在字符串生成阶段同样须要解决 props
和events
的分支。
function genData$2 (el, state) {
var data = '{';
// 曾经剖析过的 genDirectives
var dirs = genDirectives(el, state);
// 解决 props
if (el.props) {data += "domProps:" + (genProps(el.props)) + ",";
}
// 处理事件
if (el.events) {data += (genHandlers(el.events, false)) + ",";
}
}
最终 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}}})"
<input type="text" v-model="value">
如果感觉下面的流程剖析啰嗦,能够间接看上面的论断,比照模板和生成的 render
函数, 咱们能够失去:
-
input
标签所有属性,包含指令相干的内容都是以data
属性的模式作为参数的整体传入_c(即:createElement)
函数。
-
input type
的类型,在data
属性中,以attrs
键值对存在。
-
v-model
会有对应的directives
属性形容指令的相干信息。
-
- 为什么说
v-model
是一个语法糖,从render
函数的最终后果能够看出,它最终以两局部模式存在于input
标签中,一个是将value1
以props
的模式存在 (domProps
) 中,另一个是以事件的模式存储input
事件,并保留在on
属性中。
- 为什么说
-
- 重要的一个要害,事件用
$event.target.composing
属性来保障不会在输入法组合文字过程中更新数据, 这点咱们前面会再次提到。
- 重要的一个要害,事件用
11.1.4 patch 实在节点
在 patch
之前还有一个生成 vnode
的过程,这个过程没有什么特别之处,所有的包含指令,属性会以 data
属性的模式传递到构造函数 Vnode
中,最终的 Vnode
领有 directives,domProps,on
属性:
有了 Vnode
之后紧接着会执行 patchVnode
,patchVnode
过程是一个实在节点创立的过程,其中的要害是 createElm
办法,这个办法咱们在不同的场合也剖析过,后面的源码失去指令相干的信息也会保留在 vnode
的data
属性里,所以对属性的解决也会走 invokeCreateHooks
逻辑。
function createElm() {
···
// 针对指令的解决
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
的语法糖,咱们能够这样写:<input type="text" :value="message" @input="(e) => {this.message = e.target.value}" >
11.1.5 语法糖的背地
然而 v-model
仅仅是起到合并语法,创立一个新的语法糖的意义吗? 显然答案是否定的,对于须要应用输入法 (如中文、日文、韩文等) 的语言,你会发现 v-model
不会在输入法组合文字过程中失去更新。这就是 v-model
的一个重要的特点。它会在事件处理这一层增加新的事件监听compositionstart,compositionend
,他们会别离在语言输出的开始和完结时监听到变动,只有借助$event.target.composing
,就能够设计出只会在输入法组合文字的完结阶段才更新数据,这有利于进步用户的应用体验。这一部分我想借助脱离框架的表单来帮忙了解。
脱离框架的一个视图响应数据的实现(成果相似于 v -model):
// html
<input type="text" id="inputValue">
<span id="showValue"></span>
// js
<script>
let input = document.getElementById('inputValue');
let show = document.getElementById('showValue');
input.value = 123;
show.innerText = input.value
function onCompositionStart(e) {e.target.composing = true;}
function onCompositionEnd(e) {if (!e.target.composing) {return}
e.target.composing = false;
show.innerText = e.target.value
}
function onInputChange(e) {
// e.target.composing 示意是否还在输出中
if(e.target.composing)return;
show.innerText = e.target.value
}
input.addEventListener('input', onInputChange)
input.addEventListener('compositionstart', onCompositionStart)// 组合输出开始
input.addEventListener('compositionend', onCompositionEnd) // 组合输出完结
</script>
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
实质上还是一个子父组件通信的语法糖。