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这个事件 }
最后执行的el.dispatchevent(e)就会触发我们生成的render函数上定义的input事件
推荐阅读
-
Vue.js 源码分析(二十二) 指令篇 v-model指令详解
-
Vue.js 源码分析(十六) 指令篇 v-on指令详解
-
Vue.js中 v-model 指令的修饰符详解
-
Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解
-
Vue.js 源码分析(十二) 基础篇 组件详解
-
Vue.js 源码分析(二十四) 高级应用 自定义指令详解
-
Vue.js 源码分析(十五) 指令篇 v-bind指令详解
-
Vue.js 源码分析(十一) 基础篇 过滤器 filters属性详解
-
Vue.js 源码分析(十八) 指令篇 v-for 指令详解
-
Vue.js 源码分析(十三) 基础篇 组件 props属性详解