深入理解-事件委托


深入理解-事件委托

很多人是在使用事件委托的,那對於一個使用者來說,只要能正確的使用好事件委托,完成工作,就算可以了,那么你有認真的考慮過事件委托的原理,以及你的使用場景是否適合使用事件委托呢,如果需要使用事件委托,那么你是否有正確的使用呢?這里我想簡單的說一下我對事件委托的理解,希望可以有機會多多交流。

概述

事件委托有哪些好處,才會被現在人們大量的使用呢?

那么就得先說說事件的一些性能和使用的問題:

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,也就是前面源代碼中的curfor語句遍歷;

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-“類型的屬性選擇器,我先在這里說使用的方法,后面再看示例:

假設我准備要綁定事件的元素是wrapperjQuery實例化的)元素,我准備給它綁定一系列的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,相信與真正項目中的環境,更接近一些吧。

說到這里,算是結束了,如過您發下文中有描述錯誤或者不當的地方,請幫忙指正,謝謝!

本文屬於原創文章,轉載請注明出處,謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM