基本概念
事件委托,通俗地來講,就是把一個元素回應事件(click、keydown......)的函式委托到另一個元素;
一般來講,會把一個或者一組元素的事件委托到它的父層或者更外層元素上,真正系結事件的是外層元素,當事件回應到需要系結的元素上aa時,會通過事件冒泡機制從而觸發它的外層元素的系結事件上,然后在外層元素上去執行函式,
舉個例子,比如一個宿舍的同學同時快遞到了,一種方法就是他們都傻傻地一個個去領取,還有一種方法就是把這件事情委托給宿舍長,讓一個人出去拿好所有快遞,然后再根據收件人一一分發給每個宿舍同學;
在這里,取快遞就是一個事件,每個同學指的是需要回應事件的 DOM 元素,而出去統一領取快遞的宿舍長就是代理的元素,所以真正系結事件的是這個元素,按照收件人分發快遞的程序就是在事件執行中,需要判斷當前回應的事件應該匹配到被代理元素中的哪一個或者哪幾個,
事件冒泡
前面提到 DOM 中事件委托的實作是利用事件冒泡的機制,那么事件冒泡是什么呢?
在 document.addEventListener 的時候我們可以設定事件模型:事件冒泡、事件捕獲,一般來說都是用事件冒泡的模型;

如上圖所示,事件模型是指分為三個階段:
- 捕獲階段:在事件冒泡的模型中,捕獲階段不會回應任何事件;
- 目標階段:目標階段就是指事件回應到觸發事件的最底層元素上;
- 冒泡階段:冒泡階段就是事件的觸發回應會從最底層目標一層層地向外到最外層(根節點),事件代理即是利用事件冒泡的機制把里層所需要回應的事件系結到外層;### 事件
委托的優點
- 減少記憶體消耗
試想一下,若果我們有一個串列,串列之中有大量的串列項,我們需要在點擊串列項的時候回應一個事件;
<ul id="list"> <li>item 1</li> <li>item 2</li> <li>item 3</li> ...... <li>item n</li> </ul> // ...... 代表中間還有未知數個 li
如果給每個串列項一一都系結一個函式,那對于記憶體消耗是非常大的,效率上需要消耗很多性能;
因此,比較好的方法就是把這個點擊事件系結到他的父層,也就是 ul 上,然后在執行事件的時候再去匹配判斷目標元素;
所以事件委托可以減少大量的記憶體消耗,節約效率,
- 動態系結事件
比如上述的例子中串列項就幾個,我們給每個串列項都系結了事件;
在很多時候,我們需要通過 AJAX 或者用戶操作動態的增加或者去除串列項元素,那么在每一次改變的時候都需要重新給新增的元素系結事件,給即將刪去的元素解綁事件;
如果用了事件委托就沒有這種麻煩了,因為事件是系結在父層的,和目標元素的增減是沒有關系的,執行到目標元素是在真正回應執行事件函式的程序中去匹配的;
所以使用事件在動態系結事件的情況下是可以減少很多重復作業的,
jQuery 中的事件委托
jQuery 中的事件委托相信很多人都用過,它主要這幾種方法來實作:
- $.on: 基本用法: $('.parent').on('click', 'a', function () { console.log('click event on tag a'); }),它是 .parent 元素之下的 a 元素的事件代理到 $('.parent') 之上,只要在這個元素上有點擊事件,就會自動尋找到 .parent 元素下的 a 元素,然后回應事件;
- $.delegate: 基本用法: $('.parent').delegate('a', 'click', function () { console.log('click event on tag a'); }),同上,并且還有相對應的 $.delegate 來洗掉代理的事件;
- $.live: 基本使用方法: $('a', $('.parent')).live('click', function () { console.log('click event on tag a'); }),同上,然而如果沒有傳入父層元素 $(.parent),那事件會默認委托到 $(document) 上;(已廢除)
實作功能
基本實作
比如我們有這樣的一個 HTML 片段:
<ul id**=**"list"> <li>item 1</li> <li>item 2</li> <li>item 3</li> ...... <li>item n</li> </ul> // ...... 代表中間還有未知數個 li
我們來實作把 #list 下的 li 元素的事件代理委托到它的父層元素也就是 #list 上:
*// 給父層元素系結事件* document.getElementById('list').addEventListener('click', **function** (e) { *// 兼容性處理* **var** event **=** e **||** window.event; **var** target **=** event.target **||** event.srcElement; *// 判斷是否匹配目標元素* **if** (target.nodeName.toLocaleLowerCase **===** 'li') { console.log('the content is: ', target.innerHTML); } });
在上述代碼中, target 元素則是在 #list 元素之下具體被點擊的元素,然后通過判斷 target 的一些屬性(比如:nodeName,id 等等)可以更精確地匹配到某一類 #list li 元素之上;
使用 Element.matches 精確匹配
如果改變下 HTML 成:
<ul id**=**"list"> <li className**=**"class-1">item 1</li> <li>item 2</li> <li className**=**"class-1">item 3</li> ...... <li>item n</li> </ul> // ...... 代表中間還有未知數個 li
這里,我們想把 #list 元素下的 li 元素(并且它的 class 為 class-1)的點擊事件委托代理到 #list 之上;
如果通過上述的方法我們還需要在 if (target.nodeName.toLocaleLowerCase === 'li') 判斷之中在加入一個判斷 target.nodeName.className === 'class-1';
但是如果想像 CSS 選擇其般做更加靈活的匹配的話,上面的判斷未免就太多了,并且很難做到靈活性,這里可以使用 Element.matches API 來匹配;
Element.matches API 的基本使用方法: Element.matches(selectorString),selectorString 既是 CSS 那樣的選擇器規則,比如本例中可以使用 target.matches('li.class-1'),他會回傳一個布林值,如果 target 元素是標簽 li 并且它的類是 class-1 ,那么就會回傳 true,否則回傳 false;
當然它的兼容性還有一些問題,需要 IE9 及以上的現代化瀏覽器版本;
我們可以使用 Polyfill 來解決兼容性上的問題:
**if** (**!**Element.prototype.matches) { Element.prototype.matches **=**Element.prototype.matchesSelector **||**Element.prototype.mozMatchesSelector **||**Element.prototype.msMatchesSelector **||**Element.prototype.oMatchesSelector **||**Element.prototype.webkitMatchesSelector **||function**(s) { **var** matches **=** (**this**.document **||** **this**.ownerDocument).querySelectorAll(s), i **=** matches.length; **while** (**--**i **>=** 0 **&&** matches.item(i) **!==** **this**) {} **return** i **>** **-**1; }; }
加上 Element.matches 之后就可以來實作我們的需求了:
**if** (**!**Element.prototype.matches) { Element.prototype.matches **=**Element.prototype.matchesSelector **||**Element.prototype.mozMatchesSelector **||**Element.prototype.msMatchesSelector **||**Element.prototype.oMatchesSelector **||**Element.prototype.webkitMatchesSelector **||function**(s) { **var** matches **=** (**this**.document **||** **this**.ownerDocument).querySelectorAll(s), i **=** matches.length; **while** (**--**i **>=** 0 **&&** matches.item(i) **!==** **this**) {} **return** i **>** **-**1; }; } document.getElementById('list').addEventListener('click', **function** (e) { *// 兼容性處理* **var** event **=** e **||** window.event; **var** target **=** event.target **||** event.srcElement; **if** (target.matches('li.class-1')) { console.log('the content is: ', target.innerHTML); } });
函式封裝
在應對更多場景上我們可以把事件代理的功能封裝成一個公用函式,這樣就可以重復利用了,
結合上面的例子來實作一個函式 eventDelegate,它接受四個引數:
- [String] 一個選擇器字串用于過濾需要實作代理的父層元素,既事件需要被真正系結之上;
- [String] 一個選擇器字串用于過濾觸發事件的選擇器元素的后代,既我們需要被代理事件的元素;
- [String] 一個或多個用空格分隔的事件型別和可選的命名空間,如 click 或 keydown.click ;
- [Function] 需要代理事件回應的函式;
這里就有幾個關鍵點: ? 對于父層代理的元素可能有多個,需要一一系結事件; ? 對于系結的事件型別可能有多個,需要一一系結事件; ? 在處理匹配被代理的元素之中需要考慮到兼容性問題; ? 在執行所系結的函式的時候需要傳入正確的引數以及考慮到 this 的問題;
**function** eventDelegate (parentSelector, targetSelector, events, foo) { *// 觸發執行的函式* **function** triFunction (e) { *// 兼容性處理* **var** event **=** e **||** window.event; **var** target **=** event.target **||** event.srcElement; *// 處理 matches 的兼容性* **if** (**!**Element.prototype.matches) { Element.prototype.matches **=**Element.prototype.matchesSelector **||**Element.prototype.mozMatchesSelector **||**Element.prototype.msMatchesSelector **||**Element.prototype.oMatchesSelector **||**Element.prototype.webkitMatchesSelector **||function**(s) { **var** matches **=** (**this**.document **||** **this**.ownerDocument).querySelectorAll(s), i **=** matches.length; **while** (**--**i **>=** 0 **&&** matches.item(i) **!==** **this**) {} **return** i **>** **-**1; }; } *// 判斷是否匹配到我們所需要的元素上* **if** (target.matches(targetSelector)) { *// 執行系結的函式,注意 this* foo.call(target, Array.prototype.slice.call(arguments)); } } *// 如果有多個事件的話需要全部一一系結事件* events.split('.').forEach(**function** (evt) { *// 多個父層元素的話也需要一一系結* Array.prototype.slice.call(document.querySelectorAll(parentSelector)).forEach(**function** ($p) { $p.addEventListener(evt, triFunction); }); }); }
?優化
當被代理的元素不是目標元素的時候,既選擇器 targetSelector 所指向的元素不是 event.target (事件目標階段指向的元素)的時候,這時候就需要層層遍歷 event.target 的 parentNode 去匹配 targetSelector 了,直到 parentSelector,
比如:
<ul id**=**"list"> <li><span>item 1</span></li> <li><span>item 2</span></li> </ul>
還是把 li 的事件代理到 #list 之上,但這時候會發現 event.target 指向的是 li span,因此需要層層遍歷外層元素去匹配,直到到代理事件的函式,我們可以用 event.currentTarget 來獲取到代理事件的函式;
完整函式:
`function eventDelegate (parentSelector, targetSelector, events, foo) { // 觸發執行的函式 function triFunction (e) { // 兼容性處理 var event = e || window.event;
// 獲取到目標階段指向的元素
var target = event.target || event.srcElement;
// 獲取到代理事件的函式
var currentTarget = event.currentTarget;
// 處理 matches 的兼容性
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
// 遍歷外層并且匹配
while (target !== currentTarget) {
// 判斷是否匹配到我們所需要的元素上
if (target.matches(targetSelector)) {
var sTarget = target;
// 執行系結的函式,注意 this
foo.call(sTarget, Array.prototype.slice.call(arguments))
}
target = target.parentNode;
}
}
// 如果有多個事件的話需要全部一一系結事件 events.split('.').forEach(function (evt) { // 多個父層元素的話也需要一一系結 Array.prototype.slice.call(document.querySelectorAll(parentSelector)).forEach(function ($p) { $p.addEventListener(evt, triFunction); }); }); }`
使用函式:
eventDelegate('#list', 'li', 'click', **function** () { console.log(**this**); });
點擊后可以看到 console 出了 #list li 元素物件;
?局限性
當然,事件委托也是有一定局限性的;
比如 focus、blur 之類的事件本身沒有事件冒泡機制,所以無法委托;
mousemove、mouseout 這樣的事件,雖然有事件冒泡,但是只能不斷通過位置去計算定位,對性能消耗高,因此也是不適合于事件委托的;
原文鏈接:https://zhuanlan.zhihu.com/p/26536815
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/267145.html
標籤:其他
