好程序員 分享 JavaScript 事件委托代理和函數(shù)封裝詳解 , JavaScript 高級程序設(shè)計上講:事件委托就是利用事件冒泡,只指定一個事件處理程序,就可以管理某一類型的所有事件。下面我們一起看看事件代理相關(guān)介紹。
創(chuàng)新互聯(lián)堅持“要么做到,要么別承諾”的工作理念,服務(wù)領(lǐng)域包括:成都網(wǎng)站設(shè)計、成都網(wǎng)站建設(shè)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機端網(wǎng)站、網(wǎng)站推廣等服務(wù),滿足客戶于互聯(lián)網(wǎng)時代的龍子湖網(wǎng)站設(shè)計、移動媒體設(shè)計的需求,幫助企業(yè)找到有效的互聯(lián)網(wǎng)解決方案。努力成為您成熟可靠的網(wǎng)絡(luò)建設(shè)合作伙伴!
當用事件委托的時候,根本就不需要去遍歷元素的子節(jié)點,只需要給父級元素添加事件就好了,其他的都是在 js 里面的執(zhí)行,這樣可以大大的減少 dom 操作,這才是事件委托的精髓所在。
基本概念
事件委托,通俗地來講,就是把一個元素響應(yīng)事件( click 、 keydown...... )的函數(shù)委托到另一個元素;
一般來講,會把一個或者一組元素的事件委托到它的父層或者更外層元素上,真正綁定事件的是外層元素,當事件響應(yīng)到需要綁定的元素上時,會通過事件冒泡機制從而觸發(fā)它的外層元素的綁定事件上,然后在外層元素上去執(zhí)行函數(shù)。
舉個例子,比如一個宿舍的同學同時快遞到了,一種方法就是他們都傻傻地一個個去領(lǐng)取,還有一種方法就是把這件事情委托給宿舍長,讓一個人出去拿好所有快遞,然后再根據(jù)收件人一一分發(fā)給每個宿舍同學;
在這里,取快遞就是一個事件,每個同學指的是需要響應(yīng)事件的 DOM 元素,而出去統(tǒng)一領(lǐng)取快遞的宿舍長就是代理的元素,所以真正綁定事件的是這個元素,按照收件人分發(fā)快遞的過程就是在事件執(zhí)行中,需要判斷當前響應(yīng)的事件應(yīng)該匹配到被代理元素中的哪一個或者哪幾個。
事件冒泡
前面提到 DOM 中事件委托的實現(xiàn)是利用事件冒泡的機制,那么事件冒泡是什么呢?
在 document.addEventListener 的時候我們可以設(shè)置事件模型:事件冒泡、事件捕獲,一般來說都是用事件冒泡的模型;
如上圖所示,事件模型是指分為三個階段:
捕獲階段:在事件冒泡的模型中,捕獲階段不會響應(yīng)任何事件;
目標階段:目標階段就是指事件響應(yīng)到觸發(fā)事件的最底層元素上;
冒泡階段:冒泡階段就是事件的觸發(fā)響應(yīng)會從最底層目標一層層地向外到最外層(根節(jié)點),事件代理即是利用事件冒泡的機制把里層所需要響應(yīng)的事件綁定到外層; ### 事件
委托的優(yōu)點
1. 減少內(nèi)存消耗
試想一下,若果我們有一個列表,列表之中有大量的列表項,我們需要在點擊列表項的時候響應(yīng)一個事件;
......
// ...... 代表中間還有未知數(shù)個 li
如果給每個列表項一一都綁定一個函數(shù),那對于內(nèi)存消耗是非常大的,效率上需要消耗很多性能;
因此,比較好的方法就是把這個點擊事件綁定到他的父層,也就是 `ul` 上,然后在執(zhí)行事件的時候再去匹配判斷目標元素;
所以事件委托可以減少大量的內(nèi)存消耗,節(jié)約效率。
2. 動態(tài)綁定事件
比如上述的例子中列表項就幾個,我們給每個列表項都綁定了事件;
在很多時候,我們需要通過 ajax 或者用戶操作動態(tài)的增加或者去除列表項元素,那么在每一次改變的時候都需要重新給新增的元素綁定事件,給即將刪去的元素解綁事件;
如果用了事件委托就沒有這種麻煩了,因為事件是綁定在父層的,和目標元素的增減是沒有關(guān)系的,執(zhí)行到目標元素是在真正響應(yīng)執(zhí)行事件函數(shù)的過程中去匹配的;
所以使用事件在動態(tài)綁定事件的情況下是可以減少很多重復工作的。
jQuery 中的事件委托
jQuery 中的事件委托相信很多人都用過,它主要這幾種方法來實現(xiàn):
$.on: 基本用法 : $('.parent').on('click', 'a', function () { console.log('click event on tag a'); }) ,它是 .parent 元素之下的 a 元素的事件代理到 $('.parent') 之上,只要在這個元素上有點擊事件,就會自動尋找到 .parent 元素下的 a 元素,然后響應(yīng)事件;
$.delegate: 基本用法 : $('.parent').delegate('a', 'click', function () { console.log('click event on tag a'); }) ,同上,并且還有相對應(yīng)的 $.delegate 來刪除代理的事件;
$.live: 基本使用方法 : $('a', $('.parent')).live('click', function () { console.log('click event on tag a'); }) ,同上,然而如果沒有傳入父層元素 $(.parent) ,那事件會默認委托到 $(document) 上; ( 已廢除 )
實現(xiàn)功能
基本實現(xiàn)
比如我們有這樣的一個 HTML 片段:
......
// ...... 代表中間還有未知數(shù)個 li
我們來實現(xiàn)把 #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 成:
......
// ...... 代表中間還有未知數(shù)個 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 那樣的選擇器規(guī)則,比如本例中可以使用 target.matches('li.class-1') ,他會返回一個布爾值,如果 target 元素是標簽 li 并且它的類是 class-1 ,那么就會返回 true ,否則返回 false ;
當然它的兼容性還有一些問題,需要 IE9 及以上的現(xiàn)代化瀏覽器版本;
我們可以使用 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 之后就可以來實現(xiàn)我們的需求了:
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);
}
});
函數(shù)封裝
在應(yīng)對更多場景上我們可以把事件代理的功能封裝成一個公用函數(shù),這樣就可以重復利用了。
結(jié)合上面的例子來實現(xiàn)一個函數(shù) eventDelegate ,它接受四個參數(shù):
[String] 一個選擇器字符串用于過濾需要實現(xiàn)代理的父層元素,既事件需要被真正綁定之上;
[String] 一個選擇器字符串用于過濾觸發(fā)事件的選擇器元素的后代,既我們需要被代理事件的元素;
[String] 一個或多個用空格分隔的事件類型和可選的命名空間,如 click 或 keydown.click ;
[Function] 需要代理事件響應(yīng)的函數(shù);
這里就有幾個關(guān)鍵點:
對于父層代理的元素可能有多個,需要一一綁定事件;
對于綁定的事件類型可能有多個,需要一一綁定事件;
在處理匹配被代理的元素之中需要考慮到兼容性問題;
在執(zhí)行所綁定的函數(shù)的時候需要傳入正確的參數(shù)以及考慮到 this 的問題;
function eventDelegate (parentSelector, targetSelector, events, foo) {
// 觸發(fā)執(zhí)行的函數(shù)
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)) {
// 執(zhí)行綁定的函數(shù),注意 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);
});
});
}
優(yōu)化
當被代理的元素不是目標元素的時候,既選擇器 targetSelector 所指向的元素不是 event.target (事件目標階段指向的元素)的時候,這時候就需要層層遍歷 event.target 的 parentNode 去匹配 targetSelector 了,直到 parentSelector 。
比如:
還是把 li 的事件代理到 #list 之上,但這時候會發(fā)現(xiàn) event.target 指向的是 li span ,因此需要層層遍歷外層元素去匹配,直到到代理事件的函數(shù),我們可以用 event.currentTarget 來獲取到代理事件的函數(shù);
完整函數(shù):
function eventDelegate (parentSelector, targetSelector, events, foo) {
// 觸發(fā)執(zhí)行的函數(shù)
function triFunction (e) {
// 兼容性處理
var event = e || window.event;
// 獲取到目標階段指向的元素
var target = event.target || event.srcElement;
// 獲取到代理事件的函數(shù)
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;
// 執(zhí)行綁定的函數(shù),注意 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);
});
});
}
使用函數(shù):
eventDelegate('#list', 'li', 'click', function () { console.log(this); });
點擊后可以看到 console 出了 `#list li` 元素對象;
局限性
當然,事件委托也是有一定局限性的;
比如 focus 、 blur 之類的事件本身沒有事件冒泡機制,所以無法委托;
mousemove 、 mouseout 這樣的事件,雖然有事件冒泡,但是只能不斷通過位置去計算定位,對性能消耗高,因此也是不適合于事件委托的;