可以用 v-on 指令監聽 DOM 事件,併在觸發時運行一些 JavaScript 代碼,例如: 渲染結果為: 我們給測試按鈕添加了一個mouseenter和click事件,滑鼠移上去式控制台輸出: 當點擊時,輸出為: Vue的事件綁定有很多種寫法,例如: 可以看到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進行了封裝,使我們用起來更方便而已。