Vue.js 源码分析(二十二) 指令篇 v-model指令详解
Vue.js提供了v-model指令用于双向数据绑定,比如在输入框上使用时,输入的内容会事实映射到绑定的数据上,绑定的数据又可以显示在页面里,数据显示的过程是自动完成的。
v-model
本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>
<body>
<div id="app">
<p>Message is: {{message}}</p>
<input v-model="message" placeholder="edit me" type="text">
</div>
<script>
Vue.config.productionTip=false;
Vue.config.devtools=false;
new Vue({el: '#app',data(){return { message:'' }}})
</script>
</body>
</html>
渲染如下:
当我们在输入框输入内容时,Message is:后面会自动显示输入框里的内容,反过来当修改Vue实例的message时,输入框也会自动更新为该内容。
与事件的修饰符类似,v-model也有修饰符,用于控制数据同步的时机,v-model可以添加三个修饰符:lazy、number和trim,具体可以看官网。
我们如果不用v-model,手写一些事件也可以实现例子里的效果,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>
<body>
<div id="app">
<p>Message is: {{message}}</p>
<input :value="message" @input="message=$event.target.value" placeholder="edit me" type="text">
</div>
<script>
Vue.config.productionTip=false;
Vue.config.devtools=false;
new Vue({el: '#app',data(){return { message:'' }}})
</script>
</body>
</html>
我们自己手写的和用v-model有一点不同,就是当输入中文时,输入了拼音,但是没有按回车时,p标签也会显示message信息的,而用v-model实现的双向绑定是只有等到回车按下去了才会渲染的,这是因为v-model内部监听了compositionstart和compositionend事件,有兴趣的同学具体可以查看一下这两个事件的用法,网上教程挺多的。
源码分析
Vue是可以自定义指令的,其中v-model和v-show是Vue的内置指令,它的写法和我们的自定义指令是一样的,都保存到Vue.options.directives上,例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>
<body>
<script>
console.log(Vue.options.directives) //打印Vue.options.directives的值
</script>
</body>
</html>
输出如下:
Vue内部通过extend(Vue.options.directives, platformDirectives); 将v-model和v-show的指令信息保存到Vue.options.directives里面,如下:
var platformDirectives = { //第8417行 内置指令 v-module和v-show platformDirectives的意思是这两个指令和平台无关的,不管任何环境都可以用这两个指令
model: directive,
show: show
} extend(Vue.options.directives, platformDirectives); //第8515行 将两个指令信息保存到Vue.options.directives里面
Vue的源码实现代码比较多,我们一步步来,以上面的第一个例子为例,当Vue将模板解析成AST对象解析到input时会processAttrs()函数,如下:
function processAttrs (el) { //第9526行 对剩余的属性进行分析
var list = el.attrsList;
var i, l, name, rawName, value, modifiers, isProp;
for (i = 0, l = list.length; i < l; i++) { //遍历每个属性
name = rawName = list[i].name;
value = list[i].value;
if (dirRE.test(name)) { //如果该属性以v-、@或:开头,表示这是Vue内部指令
// mark element as dynamic
el.hasBindings = true;
// modifiers
modifiers = parseModifiers(name);
if (modifiers) {
name = name.replace(modifierRE, '');
}
if (bindRE.test(name)) { // v-bind //bindRD等于/^:|^v-bind:/ ,即该属性是v-bind指令时
/*v-bind逻辑*/
} else if (onRE.test(name)) { // v-on //onRE等于/^@|^v-on:/,即该属性是v-on指令时
/*v-on逻辑*/
} else { // normal directives //普通指令
name = name.replace(dirRE, ''); //去掉指令前缀,比如v-model执行后等于model
// parse arg
var argMatch = name.match(argRE); //argRE等于:(.*)$/,如果name以:开头的话
var arg = argMatch && argMatch[1];
if (arg) {
name = name.slice(0, -(arg.length + 1));
}
addDirective(el, name, rawName, value, arg, modifiers); //执行addDirective给el增加一个directives属性,值是一个数组,例如:[{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}]
if ("development" !== 'production' && name === 'model') {
checkForAliasModel(el, value);
}
}
} else {
/*普通特性的逻辑*/
}
}
}
addDirective会给AST对象增加一个directives属性,用于保存对应的指令信息,如下:
function addDirective ( //第6561行 指令相关,给el这个AST对象增加一个directives属性,值为该指令的信息,比如:
el,
name,
rawName,
value,
arg,
modifiers
) {
(el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers });
el.plain = false;
}
例子里的 <input v-model="message" placeholder="edit me" type="text">对应的AST对象如下:
接下来在generate生成rendre函数的时候,获取data属性时会执行genDirectives()函数,该函数会执行全局的model函数,也就是v-model的初始化函数,如下:
function genDirectives (el, state) { //第10352行 获取指令
var dirs = el.directives; //获取元素的directives属性,是个数组,例如:[{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}]
if (!dirs) { return } //如果没有directives则直接返回
var res = 'directives:[';
var hasRuntime = false;
var i, l, dir, needRuntime;
for (i = 0, l = dirs.length; i < l; i++) { //遍历dirs
dir = dirs[i]; //每一个directive,例如:{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}
needRuntime = true;
var gen = state.directives[dir.name]; //获取对应的指令函数,如果是v-model,则对应model函数,可能为空的,只有内部指令才有
if (gen) {
// compile-time directive that manipulates AST.
// returns true if it also needs a runtime counterpart.
needRuntime = !!gen(el, dir, state.warn); //执行指令对应的函数,也就是全局的model函数
}
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) + ']' //去掉最后的逗号,并加一个],最后返回
}
}
model()函数会根据不同的tag(select、input的不同)做不同的处理,如下:
function model ( //第6854行 v-model指令的初始化
el,
dir,
_warn
) {
warn$1 = _warn;
var value = dir.value; //值
var modifiers = dir.modifiers; //修饰符
var tag = el.tag; //标签名,比如:input
var type = el.attrsMap.type; {
// 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."
);
}
} if (el.component) {
genComponentModel(el, value, modifiers);
// component v-model doesn't need extra runtime
return false
} else if (tag === 'select') { //如果typ为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') { //如果是input标签,或者是textarea标签
genDefaultModel(el, value, modifiers); //则执行genDefaultModel()函数
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers);
// component v-model doesn't need extra runtime
return false
} else {
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.'
);
} // ensure runtime directive metadata
return true
}
genDefaultModel会在el的value绑定对应的值,并调用addHandler()添加对应的事件,如下:
function genDefaultModel ( //第6965行 nput标签 和textarea标签 el:AST对象 value:对应值
el,
value,
modifiers
) {
var type = el.attrsMap.type; //获取type值,比如text,如果未指定则为undefined // warn if v-bind:value conflicts with v-model
// except for inputs with v-bind:type
{
var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value']; //尝试获取动态绑定的value值
var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']; //尝试获取动态绑定的type值
if (value$1 && !typeBinding) { //如果动态绑定了value 且没有绑定type,则报错
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'
);
}
} var ref = modifiers || {};
var lazy = ref.lazy; //获取lazy修饰符
var number = ref.number; //获取number修饰符
var trim = ref.trim; //获取trim修饰符
var needCompositionGuard = !lazy && type !== 'range';
var event = lazy //如果有lazy修饰符则绑定为change事件,否则绑定input事件
? 'change'
: type === 'range'
? RANGE_TOKEN
: 'input'; var valueExpression = '$event.target.value';
if (trim) { //如果有trim修饰符,则在值后面加上trim()
valueExpression = "$event.target.value.trim()";
}
if (number) { //如果有number修饰符,则加上_n函数,就是全局的toNumber函数
valueExpression = "_n(" + valueExpression + ")";
} var code = genAssignmentCode(value, valueExpression); //返回一个表达式,例如:message=$event.target.value
if (needCompositionGuard) { //如果需要composing配合,则在前面加上一段if语句
code = "if($event.target.composing)return;" + code;
} //双向绑定就是靠着两行代码的
addProp(el, 'value', ("(" + value + ")")); //添加一个value的prop
addHandler(el, event, code, null, true); //添加event事件
if (trim || number) {
addHandler(el, 'blur', '$forceUpdate()');
}
}
渲染完成后对应的render函数如下:
with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v("Message is: "+_s(message))]),_v(" "),_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me","type":"text"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}})])}
我们整理一下就看得清楚一点,如下:
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
},
[_c('p', [_v("Message is: " + _s(message))]), _v(" "), _c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (message),
expression: "message"
}],
attrs: {
"placeholder": "edit me",
"type": "text"
},
domProps: {
"value": (message)
},
on: {
"input": function($event) {
if ($event.target.composing) return;
message = $event.target.value
}
}
})])
}
最后等DOM节点渲染成功后就会执行events模块的初始化事件 并且会执行directive模块的inserted钩子函数:
var directive = {
inserted: function inserted (el, binding, vnode, oldVnode) { //第7951行
if (vnode.tag === 'select') {
// #6903
if (oldVnode.elm && !oldVnode.elm._vOptions) {
mergeVNodeHook(vnode, 'postpatch', function () {
directive.componentUpdated(el, binding, vnode);
});
} else {
setSelected(el, binding, vnode.context);
}
el._vOptions = [].map.call(el.options, getValue);
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) { //如果tag是textarea节点,或者type为这些之一:text,number,password,search,email,tel,url
el._vModifiers = binding.modifiers; //保存修饰符
if (!binding.modifiers.lazy) { //如果没有lazy修饰符,先后绑定三个事件
el.addEventListener('compositionstart', onCompositionStart);
el.addEventListener('compositionend', onCompositionEnd);
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
el.addEventListener('change', onCompositionEnd);
/* istanbul ignore if */
if (isIE9) {
el.vmodel = true;
}
}
}
},
onCompositionStart和onCompositionEnd分别对应compositionstart和compositionend事件,如下:
function onCompositionStart (e) { //第8056行
e.target.composing = true;
} function onCompositionEnd (e) {
// prevent triggering an input event for no reason
if (!e.target.composing) { return } //如果e.target.composing为false,则直接返回,即保证不会重复触发
e.target.composing = false;
trigger(e.target, 'input'); //触发e.target的input事件
} function trigger (el, type) { //触发el上的type事件 例如type等于:input
var e = document.createEvent('HTMLEvents'); //创建一个HTMLEvents类型
e.initEvent(type, true, true); //初始化事件
el.dispatchEvent(e); //向el这个元素派发e这个事件
}
writer by:大沙漠 QQ:22969969
最后执行的el.dispatchEvent(e)就会触发我们生成的render函数上定义的input事件
Vue.js 源码分析(二十二) 指令篇 v-model指令详解的更多相关文章
- Vue.js 源码分析(三十二) 总结
第一次写博客,坚持了一个多月时间,Vue源码分析基本分析完了,回过头也看也漏了一些地方,比如双向绑定里的观察者模式,也可以说是订阅者模式,也就是Vue里的Dep.Watcher等这些函数的作用,网上搜 ...
- Vue.js 源码分析(三十) 高级应用 函数式组件 详解
函数式组件比较特殊,也非常的灵活,它可以根据传入该组件的内容动态的渲染成任意想要的节点,在一些比较复杂的高级组件里用到,比如Vue-router里的<router-view>组件就是一个函 ...
- spark 源码分析之十二 -- Spark内置RPC机制剖析之八Spark RPC总结
在spark 源码分析之五 -- Spark内置RPC机制剖析之一创建NettyRpcEnv中,剖析了NettyRpcEnv的创建过程. Dispatcher.NettyStreamManager.T ...
- Vue.js 源码分析(一) 代码结构
关于Vue vue是一个兴起的前端js库,是一个精简的MVVM.MVVM模式是由经典的软件架构MVC衍生来的,当View(视图层)变化时,会自动更新到ViewModel(视图模型),反之亦然,View ...
- Vue.js 源码分析(二十六) 高级应用 作用域插槽 详解
普通的插槽里面的数据是在父组件里定义的,而作用域插槽里的数据是在子组件定义的. 有时候作用域插槽很有用,比如使用Element-ui表格自定义模板时就用到了作用域插槽,Element-ui定义了每个单 ...
- Vue.js 源码分析(二十九) 高级应用 transition-group组件 详解
对于过度动画如果要同时渲染整个列表时,可以使用transition-group组件. transition-group组件的props和transition组件类似,不同点是transition-gr ...
- Vue.js 源码分析(二十八) 高级应用 transition组件 详解
transition组件可以给任何元素和组件添加进入/离开过渡,但只能给单个组件实行过渡效果(多个元素可以用transition-group组件,下一节再讲),调用该内置组件时,可以传入如下特性: n ...
- Vue.js 源码分析(二十五) 高级应用 插槽 详解
我们定义一个组件的时候,可以在组件的某个节点内预留一个位置,当父组件调用该组件的时候可以指定该位置具体的内容,这就是插槽的用法,子组件模板可以通过slot标签(插槽)规定对应的内容放置在哪里,比如: ...
- Vue.js 源码分析(二十四) 高级应用 自定义指令详解
除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令. 官网介绍的比较抽象,显得很高大上,我个人对自定义指令的理解是:当自定义指令作用在一些DOM元素或组件上 ...
随机推荐
- php laravel请求处理管道(装饰者模式)
laravel的中间件使用了装饰者模式.比如,验证维护模式,cookie加密,开启会话等等.这些处理有些在响应前,有些在响应之后,使用装饰者模式动态减少或增加功能,使得框架可扩展性大大增强. 接下来简 ...
- java获取下一天的日期
我们来看看Java怎么获取下一天的日期: 哈哈哈,开玩笑啦,这个只是个段子. "哪怕悲伤有1000种,快乐有1种就够了."
- [IDA] 将变量索引进行计算
按 k 键 [ebp+var+arg_0] - > [ebp+value]
- HTTP 状态码及含义
来自 Koa.js 官方文档中关于设置请求响应的部分 response.status=,列出了从 1xx ~ 5xx HTTP 状态码及含义,现摘录如下: 100 "continue&quo ...
- Java生鲜电商平台-SpringCloud微服务架构中网络请求性能优化与源码解析
Java生鲜电商平台-SpringCloud微服务架构中网络请求性能优化与源码解析 说明:Java生鲜电商平台中,由于服务进行了拆分,很多的业务服务导致了请求的网络延迟与性能消耗,对应的这些问题,我们 ...
- 升鲜宝V2.0_生鲜配送行业,对生鲜配送行业的思考及对系统流程开发的反思_升鲜宝生鲜配送系统_15382353715_余东升
升鲜宝V2.0_生鲜配送行业,对生鲜配送行业的思考及对系统流程开发的反思_升鲜宝生鲜配送系统_15382353715_余东升 -----生鲜配送行业现状及存在问题----- 1. 从业者整体素质偏低 ...
- 解决vue中百度地图覆盖物引用本地图片问题
这次的主要目的是在百度地图中引用自定义的覆盖物,路径是再asset/images/文件夹下,直接引用出错.需要先导入再引用. 在<script></script>代码中增加如下 ...
- bayaim_java_入门到精通_听课笔记bayaim_20181120
------------------java_入门到精通_听课笔记bayaim_20181120--------------------------------- Java的三种技术架构: JAVAE ...
- Django框架(二十二)-- Django rest_framework-解析器
一.解析器的作用 根据请求头 content-type 选择对应的解析器对请求体内容进行处理,将传过来的数据解析成字典 二.使用解析器 1.局部使用 在视图类中重定义parser_classes即可, ...
- JavaScript—Json操作
https://www.cnblogs.com/jimmyshan-study/p/11229373.html https://www.cnblogs.com/cailijuan/p/10150918 ...