Vue.js 源码分析(十六) 指令篇 v-on指令详解
可以用 v-on
指令监听 dom 事件,并在触发时运行一些 javascript 代码,例如:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>document</title> <script src="vue.js"></script> </head> <body> <div id="app"> <button @click="show('click',$event)" @mouseenter="show('mouseenter',$event)">测试</button> </div> <script> vue.config.productiontip=false; vue.config.devtools=false; var app = new vue({ el:'#app', methods:{ show(type,ev){console.log(type)} } }) </script> </body> </html>
渲染结果为:
我们给测试按钮添加了一个mouseenter和click事件,鼠标移上去式控制台输出:
当点击时,输出为:
vue的事件绑定有很多种写法,例如:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>document</title> <script src="vue.js"></script> </head> <body> <div id="app"> <p>{{message}}</p> <button @click="test1">test1</button> <!--事件可以对应一个方法--> <button @click="test2('test2',$event)">test2</button> <!--方法还可以传递参数,$event表示原始的dom事件--> <button @click="message='test3'">test3</button> <!--也可以是一个表达式--> <button @click="function(){message='test4'}">test4</button> <!--也可以是一个函数--> <button @click="()=>{message='test5'}">test5</button> <!--也可以是一个箭头函数--> </div> <script> var app = new vue({ el:'#app', data(){ return {message:"hello vue"} }, methods:{ test1(){console.log('test1');}, test2(text,ev){console.log(text);console.log(ev.type)} } }) </script> </body> </html>
可以看到v-on对应事件可以很多种格式的,可以是当前vue实例的一个方法、一个表达式、一个函数,或者一个箭头函数
源码分析
以上面的第一个例子为例,vue将dom解析成ast对象时的时候执行到a节点时会执行processelement()函数,然后会执行processattrs()函数,该函数会遍历每个属性,然后用判断是否以:或v-bind:开头,如下:
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)) { //bindrd等于/^:|^v-bind:/ ,即该属性是v-bind指令时 例如:<a :href="url">你好</a> /*这里时v-bind指令对应的分支*/ } else if (onre.test(name)) { //onre等于/^@|^v-on:/,即该属性是v-on指令时 name = name.replace(onre, ''); //获取绑定的事件类型 比如@click,此时name等于click v-on:click此时name也等于click addhandler(el, name, value, modifiers, false, warn$2); //调用addhandler()函数将事件相关信息保存到el.events或nativeevents里面 } else { // normal directives /*自定义指令的分支*/ } } else { //存储普通属性的分支 // literal attribute { var res = parsetext(value, delimiters); if (res) { warn$2( name + "=\"" + value + "\": " + 'interpolation inside attributes has been removed. ' + 'use v-bind or the colon shorthand instead. for example, ' + 'instead of <div id="{{ val }}">, use <div :id="val">.' ); } } addattr(el, name, json.stringify(value)); // #6887 firefox doesn't update muted state if set via attribute // even immediately after element creation if (!el.component && name === 'muted' && platformmustuseprop(el.tag, el.attrsmap.type, name)) { addprop(el, name, 'true'); } } } }
addhandler()函数用于给对应的ast对象增加一个events属性,保存事件对应的信息,如下:
function addhandler ( //第6573行 给el这个ast对象增加event或nativeevents,用于记录事件的信息 el, name, value, modifiers, important, warn ) { modifiers = modifiers || emptyobject; // warn prevent and passive modifier /* istanbul ignore if */ if ( "development" !== 'production' && warn && modifiers.prevent && modifiers.passive ) { warn( 'passive and prevent can\'t be used together. ' + 'passive handler can\'t prevent default event.' ); } // check capture modifier if (modifiers.capture) { delete modifiers.capture; name = '!' + name; // mark the event as captured } if (modifiers.once) { //如果有once修饰符 delete modifiers.once; name = '~' + name; // mark the event as once } /* istanbul ignore if */ if (modifiers.passive) { delete modifiers.passive; name = '&' + name; // mark the event as passive } // normalize click.right and click.middle since they don't actually fire // this is technically browser-specific, but at least for now browsers are // the only target envs that have right/middle clicks. if (name === 'click') { //鼠标按键修饰符:如果是click事件,则根据modiflers进行修正 if (modifiers.right) { name = 'contextmenu'; delete modifiers.right; } else if (modifiers.middle) { name = 'mouseup'; } } var events; if (modifiers.native) { //如果存在native修饰符,则保存到el.nativeevents里面,对于组件的自定义事件执行到这里 delete modifiers.native; events = el.nativeevents || (el.nativeevents = {}); } else { //否则保存到el.events里面 events = el.events || (el.events = {}); } var newhandler = { value: value.trim() }; if (modifiers !== emptyobject) { newhandler.modifiers = modifiers; } var handlers = events[name]; //尝试获取已经存在的该事件对象 /* istanbul ignore if */ if (array.isarray(handlers)) { //如果是数组,表示已经插入了两次了,则再把newhandler添加进去 important ? handlers.unshift(newhandler) : handlers.push(newhandler); } else if (handlers) { //如果handlers存在且不是数组,则表示只插入过一次,则把events[name]变为数组 events[name] = important ? [newhandler, handlers] : [handlers, newhandler]; } else { events[name] = newhandler; //否则表示是第一次新增该事件,则值为对应的newhandler } el.plain = false; }
例子里执行到这里这里后对应的ast等于:
接下来在generate生成rendre函数的时候会调用genhandlers函数根据不同修饰符等生成对应的属性(作为_c函数的第二个data参数一部分),
function genhandlers ( //第9992行 拼凑事件的data函数 events, isnative, warn ) { var res = isnative ? 'nativeon:{' : 'on:{'; //如果参数isnative为true则设置res为:nativeon:{,否则为:on:{ ;对于组件来说isnative为true,原生事件来说是on for (var name in events) { //遍历events,拼凑结果 res += "\"" + name + "\":" + (genhandler(name, events[name])) + ","; } return res.slice(0, -1) + '}' }
genhandler会获取每个事件对应的代码,如下:
function genhandler ( //第10004行 name:事件名,比如:name handler:事件绑定的对象信息,比如:{value: "show", modifiers: {…}} name, handler ) { if (!handler) { return 'function(){}' } if (array.isarray(handler)) { return ("[" + (handler.map(function (handler) { return genhandler(name, handler); }).join(',')) + "]") } var ismethodpath = simplepathre.test(handler.value); //是否为简单的表达式,比如show、show_d、show1等 var isfunctionexpression = fnexpre.test(handler.value); //是否为函数表达式(箭头函数或function(){}格式的匿名函数) if (!handler.modifiers) { //如果该事件的修饰符为空 if (ismethodpath || isfunctionexpression) { //如果是简单表达式或者是函数表达式 return handler.value //则直接返回handler.value,比如:show } /* istanbul ignore if */ return ("function($event){" + (handler.value) + "}") // inline statement //否则返回带有一个$event变量的函数形式,比如:当value是个表达式时,例如:value=a+123,返回格式:function($event){a+123;} } else { //如果还存在修饰符(解析模板时有些修饰符被过滤掉了) var code = ''; var genmodifiercode = ''; var keys = []; for (var key in handler.modifiers) { //遍历每个修饰符,比如:prevent if (modifiercode[key]) { //如果有在modifiercode里面定义 modifiercode是个数组,保存了一些内置修饰符对应的代码 genmodifiercode += modifiercode[key]; //则拼凑到genmodifiercode里面 // left/right if (keycodes[key]) { keys.push(key); } } else if (key === 'exact') { var modifiers = (handler.modifiers); genmodifiercode += genguard( ['ctrl', 'shift', 'alt', 'meta'] .filter(function (keymodifier) { return !modifiers[keymodifier]; }) .map(function (keymodifier) { return ("$event." + keymodifier + "key"); }) .join('||') ); } else { keys.push(key); } } if (keys.length) { //如果有按键 code += genkeyfilter(keys); //则拼凑按键 } // make sure modifiers like prevent and stop get executed after key filtering if (genmodifiercode) { code += genmodifiercode; } var handlercode = ismethodpath ? ("return " + (handler.value) + "($event)") : isfunctionexpression ? ("return (" + (handler.value) + ")($event)") : handler.value; /* istanbul ignore if */ return ("function($event){" + code + handlercode + "}") } }
例子里执行到这里后生成的render函数等于:
with(this){return _c('div',{attrs:{"id":"app"}},[_c('button',{on:{"click":function($event){show('click',$event)},"mouseenter":function($event){show('mouseenter',$event)}}},[_v("测试")])])}
其中和事件有关的如下:
on: { "click": function($event) { show('click', $event) }, "mouseenter": function($event) { show('mouseenter', $event) } }
最后在_watch渲染成真实的dom节点后,就会调用events模块的updatedomlisteners钩子函数,该函数会获取该vnode的on属性,依次遍历on对象里的每个元素,最后调用addeventlistener去绑定对应的事件
function updatedomlisteners (oldvnode, vnode) { //第7083行 domn事件相关 if (isundef(oldvnode.data.on) && isundef(vnode.data.on)) { return } var on = vnode.data.on || {}; //新node上的事件 例如:{click: ƒ ($event){}} var oldon = oldvnode.data.on || {}; target$1 = vnode.elm; //dom引用 normalizeevents(on); //处理v-model的 updatelisteners(on, oldon, add$1, remove$2, vnode.context); //调用updatelisteners做进一步处理 target$1 = undefined; }
updatelisteners()函数又会调用add$1函数去添加dom事件,如下:
function updatelisteners ( //第2036行 更新dom事件 on, oldon, add, remove$$1, vm ) { var name, def, cur, old, event; for (name in on) { //遍历on,此时name就是对应的事件类型,比如:click def = cur = on[name]; old = oldon[name]; event = normalizeevent(name); /* istanbul ignore if */ if (isundef(cur)) { "development" !== 'production' && warn( "invalid handler for event \"" + (event.name) + "\": got " + string(cur), vm ); } else if (isundef(old)) { //如果old没有定义,则表示这是一个创建事件 if (isundef(cur.fns)) { cur = on[name] = createfninvoker(cur); } add(event.name, cur, event.once, event.capture, event.passive, event.params); //调用add()绑定事件 } else if (cur !== old) { old.fns = cur; on[name] = old; } } for (name in oldon) { if (isundef(on[name])) { event = normalizeevent(name); remove$$1(event.name, oldon[name], event.capture); } } }
updatelisteners里的add函数,也就是全局的add$1函数才是最终的添加事件函数,如下:
function add$1 ( //第7052行 绑定事件 event:事件名 handler:事件的函数 once$$1:是否只执行一次 capture:是否采用捕获状态 passive:可用于移动端性能提升 event, handler, once$$1, capture, passive ) { handler = withmacrotask(handler); if (once$$1) { handler = createoncehandler(handler, event, capture); } //如果有设置了once$$1,则继续使用createoncehandler封装 target$1.addeventlistener( //调用原生的dom apiaddeventlistener添加对应的事件,2017年dom规范对addeventlistener()的第三个参数做了修订,可以是一个对象 event, handler, supportspassive ? { capture: capture, passive: passive } : capture ); }
我们看到vue内部添加dom事件最终也是通过addeventlistener()来添加的,说到底,vue只是把这些api进行了封装,使我们用起来更方便而已。
上一篇: 怎么挑大枣?贮藏条件是什么呢
下一篇: KVO-键值监听
推荐阅读
-
Vue.js 源码分析(二十二) 指令篇 v-model指令详解
-
Vue.js 源码分析(十六) 指令篇 v-on指令详解
-
Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解
-
Vue.js 源码分析(十二) 基础篇 组件详解
-
Vue.js 源码分析(二十四) 高级应用 自定义指令详解
-
Vue.js 源码分析(十五) 指令篇 v-bind指令详解
-
Vue.js 源码分析(十一) 基础篇 过滤器 filters属性详解
-
Vue.js 源码分析(十八) 指令篇 v-for 指令详解
-
Vue.js 源码分析(十三) 基础篇 组件 props属性详解
-
Vue.js 源码分析(十七) 指令篇 v-if、v-else-if和v-else 指令详解