深入理解-事件委托
很多人是在使用事件委托的,那對於一個使用者來說,只要能正確的使用好事件委托,完成工作,就算可以了,那么你有認真的考慮過事件委托的原理,以及你的使用場景是否適合使用事件委托呢,如果需要使用事件委托,那么你是否有正確的使用呢?這里我想簡單的說一下我對事件委托的理解,希望可以有機會多多交流。
概述
事件委托有哪些好處,才會被現在人們大量的使用呢?
那么就得先說說事件的一些性能和使用的問題:
1:綁定事件越多,瀏覽器內存占用越大,嚴重影響性能。
2:ajax的出現,局部刷新的盛行,導致每次加載完,都要重新綁定事件
3:部分瀏覽器移除元素時,綁定的事件並沒有被及時移除,導致的內存泄漏,嚴重影響性能
4:大部分ajax局部刷新的,只是顯示的數據,而操作卻是大部分相同的,重復綁定,會導致代碼的耦合性過大,嚴重影響后期的維護。
這些個限制,都是直接給元素事件綁定帶來的問題,所以經過了一些前輩的總結試驗,也就有了事件委托這個解決方案。
我們本篇將要說的是,事件委托。
事件委托的基礎
如果我們相對一個技術點了解的更深,用的更好,那么我們就需要對這個技術點的原理有更多的了解,那么事件委托的實現原理是什么呢?
1:事件的冒泡,所以才可以在父元素來監聽子元素觸發的事件。
2:DOM的遍歷,一個父級元素包含的子元素過多,那么當一個事件被觸發時,是否觸發了某一種類型的元素呢?
這是事件委托的兩個基礎,也是事件委托的核心,跟事件委托相關的技術點,如果碰到什么問題,都可以在這兩個點進行切入,來尋求解決方案。
而且還有一點要注意:不管你使用什么樣的框架,實現方案,這兩個基礎都是必須的,OK,我們繼續看下去。
一個簡單的事件委托
只是使用文字描述,是無法很好的理解事件委托的,那么這里我們來看一個例子:
注:假設只支持標准瀏覽器,不兼容IE的低版本
我現在使用原生的JS,來實現一個簡單的事件委托
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
function _addEvent(obj,type,fn){ obj.addEventListener(type,fn,false); } function _delegate(obj,className,fn){ var dc = " "+className+ " "; function cb(e){ var target = e.target, c = ""; while(target != obj){ c = " "+target.getAttribute("class")+" "; if(c.indexOf(dc) != -1){ fn.call(target,e); } target = target.parentNode; } } _addEvent(obj,"click",cb); }
然后,可以直接這么調用:_delegate(document.body,"item",fn);
它執行的效果是:body內部,所有class包含item的元素,都會相應該操作。
查看示例:DEMO
注:該方法是為了說明這個原理,並不是用於生產開發中的,如果想要用在生產開發中,那么實現方式應該更嚴謹,一些必要的類型檢測,還是需要的。
jQuery中的事件委托的實現
我前面說的,不管使用什么樣的技術方案,都不能拋開事件委托的兩個基礎,那么我們就看看jQuery庫的實現方法吧(其他的庫,都沒有去看,汗~~);
暫且不論事件綁定,各個地方是如何處理的,當事件冒泡到綁定的元素上時,要做出相應的時候,會有下面的一段函數:
jQuery.event.handlers函數,用來查看所有包含事件委托,和直接綁定的回調函數的,源代碼如下:(源代碼來自jQuery v3.1.1版本)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
//前面是一些判斷,判斷如果該元素之前有被綁定過事件委托, //並且符合一些其他的限制(比如:點擊不是右鍵,元素不是txt元素等)的時候, //就會執行到這里: //cur = event.target // //cur,直接從target自this的DOM遍歷 for ( ; cur !== this; cur = cur.parentNode || this ) { // Don't check non-elements (#13208) // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) // 判斷是否為對應的元素,Element元素, // type = "click"的元素,不能被disabled。 if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { matchedHandlers = []; matchedSelectors = {}; //delegateCount表示,該元素,被綁定了多少次事件委托, //把這些個委托事件,都遍歷一遍 for ( i = 0; i < delegateCount; i++ ) { handleObj = handlers[ i ]; // Don't conflict with Object.prototype properties (#13203) sel = handleObj.selector + " "; if ( matchedSelectors[ sel ] === undefined ) { matchedSelectors[ sel ] = handleObj.needsContext ? jQuery( sel, this ).index( cur ) > -1 : jQuery.find( sel, this, null, [ cur ] ).length; } //如果符合,則把回調函數推入一個數組中。 if ( matchedSelectors[ sel ] ) { matchedHandlers.push( handleObj ); } } //如果當前的cur元素,找到了需要回調的函數,那么就把相關的數據, //推入到handlerQueue數組中,在最后handlerQueue會被返回 //在另外的dispatch函數中,按順序執行,來觸發這些回調 if ( matchedHandlers.length ) { handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); } } }
從上面的代碼中,來驗證,jQuery中,事件委托的原理同樣離不開DOM的查找。
那么同樣,你是否有注意到,在事件委托中,到底執行了多少次的DOM查找呢?
1:從目標event.target到綁定事件的元素ele之間,有多少層的DOM結構,假設為x,也就是前面源代碼中的cur的for語句遍歷;
2:在ele元素上,綁定過多少次的事件委托,假設為y,也就是前面源代碼中的delegateCount數據。
那么在每次觸發ele區域的type事件之后,就需要遍歷的DOM結構的次數是x*y;也就是源代碼中,兩個for語句執行的次數。
如果按照這個計算來看,那么層級越多,事件委托的綁定次數越多,那么在每次觸發type事件時,需要查找DOM的次數就越多。
事件委托的缺點
說到這里,還有一個問題就是,我們應該都知道,JS的運行速度還是很不錯的,尤其是一些現代瀏覽器,而瀏覽器中的DOM操作,卻是非常耗費性能的,那么在事件委托的時候,這些DOM操作,是否會影響整個頁面的運行性能呢?
這無疑是肯定的,前面,我們根據jQuery的源碼看到了,DOM遍歷的次數與DOM結構的層數,和事件委托綁定的個數有關。
這個說法對於click這樣的事件來說,消耗還算少的話;
那么對於隨時都會觸發的mouseover等事件來說,這個消耗,是否看起來就比較可觀了呢?
如果再考慮到一些性能不好設備,使用了性能不好的瀏覽器呢?這個消耗又會是怎么樣的呢?
綜合上述的考慮,你是否願意,認真的考慮一下,在使用事件委托的時候,是否符合你的使用場景呢,是否真的有必要,隨意的去使用事件委托呢?
先看兩個例子吧:
同樣使用jQuery的事件委托,同樣是100個元素:
1:使用一次事件委托,委托到所有的元素- DEMO
2:使用100個事件委托,每個都委托一個元素 – DEMO
這個是一個簡單的例子,也屬於比較極端的例子,只是為了驗證這個東西,我使用timeline測試一次點擊事件,耗費的時間比,得到的結果如下圖所示:
使用一次事件委托,委托到所有的元素

使用100個事件委托,每個都委托一個元素

這還是在沒有其他事件的情況下:
接下來我們看看,如果我們監聽的是mouseover這個事件呢?
測試DEMO的鏈接:
1:使用一次事件委托,委托到所有的元素- DEMO
2:使用100個事件委托,每個都委托一個元素 – DEMO
得到的數據:
使用一次事件委托,委托到所有的元素:

使用100個事件委托,每個都委托一個元素

如果是這樣的話,那這個消耗是否看起來更可觀了,這里的情況還比較單一,如果再一個很復雜的頁面,交叉着使用這些呢?
什么時候選擇使用事件委托
完美是不存在的,任何的東西都有它的兩面性,都是有好有壞,選擇一個就要在擁有它的好的同時,接受它的壞的地方,就像是男女之間,如果都想找那個完美的另一半,那么還是選擇孤獨終老吧(這個應該更簡單),所以這個時候,只要我們能看到好的同時,也可以接受那一些不好的,退一步海闊天空嘛~~~
所以,事件委托也是這樣的,如果事件委托沒有缺點,那么它就不僅僅是一個解決方案了,而是會被瀏覽器直接納入規范了吧,那么當前的事件綁定規范,就要直接改掉了
既然如此,那么什么時候,才適合使用事件委托呢,如何能更優的使用呢?
結合前面我們說到的,事件委托影響性能的因素:
1:元素中,綁定事件委托的次數;
2:點擊的最底層元素,到綁定事件元素之間的DOM層數;
結合這三點,在必須使用事件委托的地方,可以如下的處理:
1:只在必須的地方,使用事件委托,比如:ajax的局部刷新區域
2:盡量的減少綁定的層級,不在body元素上,進行綁定
3:減少綁定的次數,如果可以,那么把多個事件的綁定,合並到一次事件委托中去,由這個事件委托的回調,來進行分發。
說到這里,也只能算是有了一個最基礎的結論,但是呢?總的有個解決方案吧,不然…
提高事件委托性能的解決方案
看完前面的事件委托的一些瓶頸之外,現在要給出一些解決的方案了:
1:降低層級,這個比較好實現,在開發中,直接把事件綁定在低層級的元素上即可,這個無法繼續優化;
2:減少綁定的次數,現在只能在這個點上繼續優化了。
所以,在這里,來看看我的解決方案(基於jQuery/Zepto的),在我的解決方案中,我固化了一些東西,比如,使用事件委托時,不在使用class等一些常用的選擇器,而是使用”data-“類型的屬性選擇器,我先在這里說使用的方法,后面再看示例:
假設我准備要綁定事件的元素是wrapper(jQuery實例化的)元素,我准備給它綁定一系列的click事件,那么就需要如下的使用方法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
var wrapper = $("#wrapper") wrapperClick = eventMediator(wrapper,"click"); //給wrapper初始化一個click的事件委托 //那么表示,在wrapper元素子元素中,只要元素中有data-click的元素,會被覆蓋 //添加一個click1的回調,那么表示如果點擊的目標元素中,包含data-click="click1"的元素 //可以執行該回調 wrapperClick.add("click1",function(){}); wrapperClick.add("click2",function(){}); wrapperClick.add("click3",function(){}); //這個,我們在wrapper元素上,綁定的事件委托,其實就是有三種回調,那么 //當元素當元素的而具體執行哪一個回調,就與子元素的data-click的屬性值有關 //data-click = "click1"的元素,執行綁定的第一個回調 //data-click = "click2"的元素,執行綁定的第二個回調 //data-click = "click3"的元素,執行綁定的第三個回調
如此,則可以實現,在一個元素上,綁定一次事件委托,可以根據data-click的不同,執行不同的回調。
其中eventMediator方法中,返回的對象,除了包含有add方法(注冊一個回調)之外,還包含一個移除的方法,remove方法,通過remove方法(使用方法與add一樣,傳的參數也一樣),可以直接移除之前的一個注冊(匿名函數不能被移除)。
使用這樣的方法,就可以做到雖然我這個區域,不同的元素需要不同的回調函數,而我也只需要一個事件委托,就可以解決這個問題,那么事件委托中,每次觸發事件導致的DOM查找,就只受限於DOM的層數了,這也就可以有效的降低了因為DOM查找帶來的損耗了,接下來我們看看一些對比:
1:click事件,100次不同的回調
直接使用jQuery的事件委托:DEMO
優化后事件委托的DEMO :DEMO
直接使用jQuery事件委托:

優化后的事件委托:

2:mouseover事件,100次不同的回調
直接使用jQuery的DEMO:DEMO
優化后的DEMO :DEMO
直接使用jQuery事件委托:

優化后的事件委托:

至於具體的使用方法,請查看DEMO哦,以及源代碼的實現方式,都可以在DEMO找到的。
並且,您可以試試,回調函數,和直接使用jQuery綁定時的回調函數,有什么區別,說不定你會愛上這個方案呢,哈~~
結尾
我這里的DEMO雖然把綁定回調的函數設置為100個,雖然一個項目中,事件委托的個數不會有這么多,但是一個真正的項目,所處的環境,畢竟會比這里的DEMO復雜好多,所以這里就把這個設置為100,相信與真正項目中的環境,更接近一些吧。
說到這里,算是結束了,如過您發下文中有描述錯誤或者不當的地方,請幫忙指正,謝謝!
本文屬於原創文章,轉載請注明出處,謝謝!
