上一篇文章介紹了Vue.js內部的整體結構,知道了它會向構造函數添加一些屬性和方法。
在Vue.js內部有這樣一段代碼
其中定義了Vue構造函數,然后分別調用initMiXin,stateMixin,eventsMixin,lifecycleMixin,renderMixin,並將Vue構造函數當作參數傳給這5個函數
這5個函數的作用就是向Vue的原型上掛載方法。
當函數initMixin被調用時,會向Vue構造函數的prototype屬性添加_init方法,執行new Vue()時,會調用_init方法。
好,接下來進入到本文的標題,與事件相關的實例方法有4個,分別是,vm.$on , vm.$emit,vm.$once,vm.$off;
這四個方法實在eventsMixin中掛在到Vue構造函數和prototype屬性中的。
當eventsMixin被調用的時候,會向Vue構造函數的prototype屬性添加4個實例方法。
下面將介紹一下這四種方法
1、vm.$on
vm.$on(event,callback)
- 參數:
{string | Array<string>} event
(數組只在 2.2.0+ 中支持){Function} callback
用法:監聽當前實例上的自定義事件,事件可以由vm.$emit觸發,回調函數會接收所有傳入事件所觸發的函數的額外參數
原理:只需要在注冊事件時將回調函數收集起來,在觸發事件時將收集起來的回調函數依次調用即可,代碼如下
Vue.prototype.$on = function (event, fn) { var vm = this; if (Array.isArray(event)) { for (var i = 0, l = event.length; i < l; i++) { vm.$on(event[i], fn); } } else { (vm._events[event] || (vm._events[event] = [])).push(fn); } return vm };
當event為數組的時候,需要遍歷數組,使得數組中的每一項都調用vm.$on,使回調可以注冊到數組中每項事件名所指定的事件列表中,當event參數不為數組時,就像事件列表中添加回調。
vm._events是一個對象,用來存儲事件,使用事件名(event)從vm._events中取出事件列表,如果列表不存在,則使用空數組初始化,然后將回調函數添加到事件列表中。
vm._events在執行new Vue()時,會執行this._init方法進行一系列操作,其中就在vue.js的實例上創建一個_events
2、vm.$emit
vm.$emit(eventName,[...args])
參數:
{string} eventName
[...args]
用法:觸發當前實例上的事件,附加參數都會傳給監聽器回調。
原理:vm.$emit作用是觸發事件,所有的事件監聽器回調都會存儲在vm._events中,所以觸發事件的實現思路是使用事件名從vm._events中取出對應的事件監聽器回調
列表,然后依次執行列表中的監聽器回調並將參數傳遞給監聽器回調,代碼如下
Vue.prototype.$emit = function (event) { var vm = this; var cbs = vm._events[event]; if (cbs) { // const args = toArray(arguments,1); cbs = cbs.length > 1 ? toArray(cbs) : cbs; var args = toArray(arguments, 1); var info = "event handler for \"" + event + "\""; 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 };
使用event從vm._events中取出事件監聽器回調函數列表,並將其賦值給變量cbs,如果cbs存在,依次調用每一個監聽器回調並將其所有參數傳給監聽器回調。
toArray的作用是將類似於數組的數據換成真正的數組,它的第二個參數是起始位置,也就是說,args是一個數組,里面包含除第一個參數之外的所有參數。
$on和$emit可以結合起來使用,大部分用於父子組件傳值,但是這里有一個問題
1、究竟是由子組件內部主動傳數據給父組件,由父組件監聽接收(由子組件中操作決定什么時候傳值)
2、還是通過父組件決定子組件什么時候傳值給父組件,然后再監聽接收 (由父組件中操作決定什么時候傳值)
兩種情況都有
$meit事件觸發,通過子組件內部的事件觸發自定義事件$emit
$meit事件觸發, 可以通過父組件操作子組件 (ref)的事件來觸發 自定義事件$emit
第一種情況
父組件 所以msg打印出來就是蠟筆小柯南
子組件
第二種情況
父組件 通過ref操作子組件觸發事件
子組件
2、vm.$off([event,callback])
參數:
{string | Array<string>} event
(只在 2.2.2+ 支持數組){Function} [callback]
用法:
移除自定義事件監聽器。
-
如果沒有提供參數,則移除所有的事件監聽器;
-
如果只提供了事件,則移除該事件所有的監聽器;
-
如果同時提供了事件與回調,則只移除這個回調的監聽器。
Vue.prototype.$off = function(event,fn){ const vm = this; //1-1、沒有提供參數的情況,此時需要移除所有事件的監聽器 if(!arguments.length){ // 當arguments.lengtha為0時,說明沒有任何參數,這時需要移除所有事件監聽器 // 因此重置了vm._events屬性,vm._events存儲所有事件,所以將vm._events重置 // 為初始狀態就等同於將所有事件都移除了。 vm._events = Object.create(null); return vm; } // 1-2、vm.$off的第一個參數支持數組,當event為數組的時候,只需要將數組遍歷一遍,然后 // 數組中的每一項依次調用vm.$off; if(Array.isArray(event)){ for(let i = 0 ; i<event.length; i++){ this.$off(event[i],fn) } return vm; } // 2、只提供了事件名,則移除該事件所有的監聽器,只需要將vm._events中的event重置為空就行 const cbs = vm._events[event]; if(!cbs){ return vm; } // 這里做了一個安全檢測,如果這個事件沒有被監聽,vm._events內找不到任何監聽器,直接退出 // 程序,然后判斷是否只有一個參數,如果是,將事件名在vm._events中所有事件都移除,只需要 // 將vm._events上以該事件為屬性的值設置為null即可 if(arguments.length === 1){ vm._events[event] = null; return vm; } // 3、如果同時提供了事件與回調,那么只移除這個回調的監聽器,將使用參數中提供的事件名從 // vm._events取出事件列表,然后從列表中找到與參數中提供的回調函數相同的那個函數,並 // 將它從列表中移除 if(fn){ // 先判斷是否有fn參數,有則說明用戶同時提供了event和fn兩個參數,然后從vm._events // 中取出事件監聽器列表並遍歷它,如果列表中的某一項與fn相同,或者某一項的fn屬性與fn相同, // 使用splice方法將它從列表中移除,當循環結束后,列表中所有與用戶參數中提供的fn相同的監聽器 // 都會被移除 const cbd = vm._events[event]; let cb; let i = cbs.length; while(i--){ // 這里有一個細節要注意,在代碼中遍歷列表是從后向前循環,這樣在列表中移除當前 // 位置的監聽器,不會影響列表中未遍歷到的監聽器位置,如果是從前向后遍歷,那么當從 // 列表中移除一個監聽器時,后面的監聽器會自動向前移動一個位置,會導致下一輪循環 // 時跳過一個元素。 cb = cbs[i]; if(cb === fn || cb.fn === fn){ cbs.splice(i,1); break; } } } return vm; }
2、vm.$once(event,callback)
參數:
{string} event
{Function} callback
用法:監聽一個自定義事件,但是只觸發一次。一旦觸發之后,監聽器就會被移除
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 };
在vm.$once中調用vm.$on來實現監聽自定義事件的功能,當自定義事件觸發后會執行攔截器,將監聽器從事件列表中移除。
在vm.$once使用vm.$on來監聽事件,首先,將函數on注冊到事件中,當自定義事件被觸發時,會執行函數on(在這個函數中,會用時vm.$off將自定義事件移除),然后
手動執行函數,並將參數arguments傳遞給函數fn,這樣就可以實現vm.$once的功能。
但是要注意on.fn = fn這行代碼,前面介紹vm.$off時提到,在移除監聽器時,需要將用戶提供的監聽器函數與列表中的監聽器函數進行對比,相同部分會被移除,這導致當我們使用攔截器代替監聽器注入到事件列表中時,
攔截器和用戶提供的函數時不相同的,此時用戶使用vm.$off來移除事件監聽器,移除操作失敗。
這個問題的解決方案是將用戶提供的原始監聽器保存到攔截器的fn屬性中,當vm.$off方法遍歷事件監聽器列表時,同時會檢查監聽器和監聽器的fn屬性是否與用戶提供的監聽器函數相同,只要有一個相同,就說明需要被移除
的監聽器找到了,將被找到的攔截器從監聽器列表中移除
以上就是與vue中與實例相關的方法的總結。有不當的地方還請包含。日后一定改進。