Vue定義了四種添加事件監聽的方法:
例如$on
,可經過vm.$on(event,callback)
添加監聽。javascript
Vue.prototype.$on = function (event, fn) { var this$1 = this; var vm = this; //若是傳參event是數組,遞歸調用$on if (Array.isArray(event)) { for (var i = 0, l = event.length; i < l; i++) { this$1.$on(event[i], fn); } } else { (vm._events[event] || (vm._events[event] = [])).push(fn); // 這裏在註冊事件的時候標記bool值也就是個標誌位來代表存在鉤子,而不須要經過哈希表的方法來查找是否有鉤子,這樣作能夠減小沒必要要的開銷,優化性能。 if (hookRE.test(event)) { vm._hasHookEvent = true; } } return vm };
$once監聽只能觸發一次的事件,觸發之後會自動移除該事件。html
Vue.prototype.$once = function (event, fn) { var vm = this; function on () { //在第一次執行的時候將該事件銷燬 vm.$off(event, on); //執行註冊的方法 fn.apply(vm, arguments); } on.fn = fn; vm.$on(event, on); return vm };
$off用來移除自定義事件。vue
Vue.prototype.$off = function (event, fn) { var this$1 = this; var vm = this; // 若是沒有參數,關閉所有事件監聽器 if (!arguments.length) { vm._events = Object.create(null); return vm } // 關閉數組中的事件監聽器 if (Array.isArray(event)) { for (var i = 0, l = event.length; i < l; i++) { this$1.$off(event[i], fn); } return vm } // 具體的某個事件 var cbs = vm._events[event]; if (!cbs) { return vm } // fn回調函數不存在,將事件監聽器變爲null,返回vm if (!fn) { vm._events[event] = null; return vm } // 回調函數存在 if (fn) { // specific handler var cb; var i$1 = cbs.length; while (i$1--) { cb = cbs[i$1]; if (cb === fn || cb.fn === fn) { // 移除 fn 這個事件監聽器 cbs.splice(i$1, 1); break } } } return vm };
$emit用來觸發指定的自定義事件。java
Vue.prototype.$emit = function (event) { var vm = this; { var lowerCaseEvent = event.toLowerCase(); if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { tip( "Event \"" + lowerCaseEvent + "\" is emitted in component " + (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " + "Note that HTML attributes are case-insensitive and you cannot use " + "v-on to listen to camelCase events when using in-DOM templates. " + "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"." ); } } var cbs = vm._events[event]; if (cbs) { //將類數組的對象轉換成數組 cbs = cbs.length > 1 ? toArray(cbs) : cbs; var args = toArray(arguments, 1); for (var i = 0, l = cbs.length; i < l; i++) { try { //觸發當前實例上的事件,附加參數都會傳給監聽器回調。 cbs[i].apply(vm, args); } catch (e) { handleError(e, vm, ("event handler for \"" + event + "\"")); } } } return vm };
先來研究發生在原生dom元素上的事件。
用v-on指令監聽DOM事件,並在觸發時運行一些JS代碼,也能夠經過@簡寫的方式,緣由:
Vue定義了這樣的正則表達式var onRE = /^@|^v-on:/;
,在processAttrs()解析屬性時經過onRE.test(name)
判斷來添加屬性。
html的編寫以下:node
<body> <div id="app"> <button @click="test01">點我01號</button> </div> <script> new Vue({ 'el':'#app', methods:{ test01:function(){ alert('01號被點了!'); } } }); </script> </body>
Vue初始化時,調用initEvents(vm)對事件進行初始化:正則表達式
function initEvents (vm) { vm._events = Object.create(null); //_hasHookEvent標誌位代表是否存在鉤子,而不須要經過哈希表來查找是否有鉤子,這樣作能夠減小沒必要要的開銷,優化性能。 vm._hasHookEvent = false; //初始化父組件attach的事件 var listeners = vm.$options._parentListeners; if (listeners) { updateComponentListeners(vm, listeners); } }
initEvents方法在vm上建立一個_events對象,用來存放事件。
接下來就調用initState()方法初始化事件,相關代碼段:if (opts.methods) { initMethods(vm, opts.methods); }
這個例子methods內有方法test01,所以會執行initMethods。segmentfault
vm[key] = methods[key] == null ? noop : bind(methods[key], vm);
initMethods遍歷定義的methods,經過調用bind改變函數的this指向,修飾了事件的回調函數,組件上掛載的事件都是在父做用域中的。數組
function nativeBind (fn, ctx) { //fn的this指向ctx return fn.bind(ctx) } var bind = Function.prototype.bind ? nativeBind : polyfillBind;
這裏bind用來改變函數中this的指向。(關於call,apply,bind的學習來自於https://www.cnblogs.com/xljzl...)
接下來Vue將html解析成ast,解析後的render如圖所示緩存
解析過程參考https://segmentfault.com/a/11...
接下來調用add$1經過addEventListener將事件綁定到target上。app
function add$1 (event,handler,once$$1,apture,passive) { handler = withMacroTask(handler); if (once$$1) { handler = createOnceHandler(handler, event, capture); } //綁定點擊事件 target$1.addEventListener( event, handler, supportsPassive ? { capture: capture, passive: passive } : capture ); }
<body> <div id="app"> <child-test :test-message="mess" v-on:click.native="test01" v-on:componenton="test02"></child-test> </div> <template id="tplt01"> <button>{{testMessage}}</button> </template> <script> new Vue({ 'el':'#app', data:{mess:'組件點擊001'}, methods:{ test01:function(){ alert('01號被點了!'); }, test02:function(){ alert('01號被點了!'); } }, components:{ 'childTest':{ template:"#tplt01", props:['testMessage'], methods:{ test02:function(){ alert('001號被點了!'); } }, } } }); </script> </body>
<child-test>標籤訂義了click事件,若click事件沒加修飾符.native,點擊按鈕不出發任何事件,
若添加了.native修飾符,點擊按鈕執行的就是test01,alert('01號被點了!');
。.native的做用就是在原生dom上綁定事件。
不添加.native的的事件解析過程與上文相同,而添加了.native以後,v-on的事件會被放到nativeOn數組中,解析後的render如圖所示:
在事件初始化,調用genHandlers的時候,會先判斷該事件是否爲native,若是是,解析的事件字符串就會用'nativeOn{}'包裹。
function genHandlers ( events, isNative, warn ) { var res = isNative ? 'nativeOn:{' : 'on:{'; for (var name in events) { res += "\"" + name + "\":" + (genHandler(name, events[name])) + ","; } return res.slice(0, -1) + '}' }
html解析成vnode以後會調用createComponent進行處理。
function createComponent ( Ctor, data, context, children, tag ) { if (isUndef(Ctor)) { return } var baseCtor = context.$options._base; /*其餘代碼省略*/ //緩存data.on的函數,這些須要做爲子組件監聽器而不是DOM監聽器來處理。就是componenton事件 var listeners = data.on; //data.on被native修飾符的事件所替換 data.on = data.nativeOn; /*其餘代碼省略*/ var vnode = new VNode( ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')), data, undefined, undefined, undefined, context, { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }, asyncFactory ); return vnode }
createComponent將.native修飾符的事件放在data.on上面。接下來data.on上的事件(本文中的alert('001號被點了!');)會按普通的html事件往下走。
function updateDOMListeners (oldVnode, vnode) { if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { return } var on = vnode.data.on || {}; var oldOn = oldVnode.data.on || {}; target$1 = vnode.elm; normalizeEvents(on); updateListeners(on, oldOn, add$1, remove$2, vnode.context); target$1 = undefined; }
最終經過target$1.addEventListener添加事件監聽。
而標籤內沒有.native的修飾符調用的是$on方法。
function updateComponentListeners ( vm, listeners, oldListeners ) { target = vm; updateListeners(listeners, oldListeners || {}, add, remove$1, vm); target = undefined; }
updateListeners又調用了add方法
function add (event, fn, once) { if (once) { target.$once(event, fn); } else { target.$on(event, fn); } }
也就是說對於普通html元素和在組件標籤內添加了.native修飾符的事件,都經過target$1.addEventListener()來掛載事件。而定義在組件上的事件會調用原型上的$on等方法。
關於事件的使用官網已經說得很清楚啦,這裏就不贅述啦。
https://cn.vuejs.org/v2/guide...
<!-- 阻止單擊事件繼續傳播 --> <a v-on:click.stop="doThis"></a> <!-- 提交事件再也不重載頁面 --> <form v-on:submit.prevent="onSubmit"></form> <!-- 修飾符能夠串聯 --> <a v-on:click.stop.prevent="doThat"></a> <!-- 只有修飾符 --> <form v-on:submit.prevent></form> <!-- 添加事件監聽器時使用事件捕獲模式 --> <!-- 即元素自身觸發的事件先在此到處理,而後才交由內部元素進行處理 --> <div v-on:click.capture="doThis">...</div> <!-- 只當在 event.target 是當前元素自身時觸發處理函數 --> <!-- 即事件不是從內部元素觸發的 --> <div v-on:click.self="doThat">...</div>