好程序員 分享 JavaScript 事件委托代理和函數(shù)封裝詳解 , JavaScript 高級(jí)程序設(shè)計(jì)上講:事件委托就是利用事件冒泡,只指定一個(gè)事件處理程序,就可以管理某一類型的所有事件。下面我們一起看看事件代理相關(guān)介紹。
當(dāng)用事件委托的時(shí)候,根本就不需要去遍歷元素的子節(jié)點(diǎn),只需要給父級(jí)元素添加事件就好了,其他的都是在 js 里面的執(zhí)行,這樣可以大大的減少 dom 操作,這才是事件委托的精髓所在。
基本概念
事件委托,通俗地來講,就是把一個(gè)元素響應(yīng)事件( click 、 keydown...... )的函數(shù)委托到另一個(gè)元素;
一般來講,會(huì)把一個(gè)或者一組元素的事件委托到它的父層或者更外層元素上,真正綁定事件的是外層元素,當(dāng)事件響應(yīng)到需要綁定的元素上時(shí),會(huì)通過事件冒泡機(jī)制從而觸發(fā)它的外層元素的綁定事件上,然后在外層元素上去執(zhí)行函數(shù)。
舉個(gè)例子,比如一個(gè)宿舍的同學(xué)同時(shí)快遞到了,一種方法就是他們都傻傻地一個(gè)個(gè)去領(lǐng)取,還有一種方法就是把這件事情委托給宿舍長(zhǎng),讓一個(gè)人出去拿好所有快遞,然后再根據(jù)收件人一一分發(fā)給每個(gè)宿舍同學(xué);
在這里,取快遞就是一個(gè)事件,每個(gè)同學(xué)指的是需要響應(yīng)事件的 DOM 元素,而出去統(tǒng)一領(lǐng)取快遞的宿舍長(zhǎng)就是代理的元素,所以真正綁定事件的是這個(gè)元素,按照收件人分發(fā)快遞的過程就是在事件執(zhí)行中,需要判斷當(dāng)前響應(yīng)的事件應(yīng)該匹配到被代理元素中的哪一個(gè)或者哪幾個(gè)。
事件冒泡
前面提到 DOM 中事件委托的實(shí)現(xiàn)是利用事件冒泡的機(jī)制,那么事件冒泡是什么呢?
在 document.addEventListener 的時(shí)候我們可以設(shè)置事件模型:事件冒泡、事件捕獲,一般來說都是用事件冒泡的模型;
如上圖所示,事件模型是指分為三個(gè)階段:
捕獲階段:在事件冒泡的模型中,捕獲階段不會(huì)響應(yīng)任何事件;
目標(biāo)階段:目標(biāo)階段就是指事件響應(yīng)到觸發(fā)事件的最底層元素上;
冒泡階段:冒泡階段就是事件的觸發(fā)響應(yīng)會(huì)從最底層目標(biāo)一層層地向外到最外層(根節(jié)點(diǎn)),事件代理即是利用事件冒泡的機(jī)制把里層所需要響應(yīng)的事件綁定到外層; ### 事件
委托的優(yōu)點(diǎn)
1. 減少內(nèi)存消耗
試想一下,若果我們有一個(gè)列表,列表之中有大量的列表項(xiàng),我們需要在點(diǎn)擊列表項(xiàng)的時(shí)候響應(yīng)一個(gè)事件;
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
// ...... 代表中間還有未知數(shù)個(gè) li
如果給每個(gè)列表項(xiàng)一一都綁定一個(gè)函數(shù),那對(duì)于內(nèi)存消耗是非常大的,效率上需要消耗很多性能;
因此,比較好的方法就是把這個(gè)點(diǎn)擊事件綁定到他的父層,也就是 `ul` 上,然后在執(zhí)行事件的時(shí)候再去匹配判斷目標(biāo)元素;
所以事件委托可以減少大量的內(nèi)存消耗,節(jié)約效率。
2. 動(dòng)態(tài)綁定事件
比如上述的例子中列表項(xiàng)就幾個(gè),我們給每個(gè)列表項(xiàng)都綁定了事件;
在很多時(shí)候,我們需要通過 ajax 或者用戶操作動(dòng)態(tài)的增加或者去除列表項(xiàng)元素,那么在每一次改變的時(shí)候都需要重新給新增的元素綁定事件,給即將刪去的元素解綁事件;
如果用了事件委托就沒有這種麻煩了,因?yàn)槭录墙壎ㄔ诟笇拥模湍繕?biāo)元素的增減是沒有關(guān)系的,執(zhí)行到目標(biāo)元素是在真正響應(yīng)執(zhí)行事件函數(shù)的過程中去匹配的;
所以使用事件在動(dòng)態(tài)綁定事件的情況下是可以減少很多重復(fù)工作的。
jQuery 中的事件委托
jQuery 中的事件委托相信很多人都用過,它主要這幾種方法來實(shí)現(xiàn):
$.on: 基本用法 : $('.parent').on('click', 'a', function () { console.log('click event on tag a'); }) ,它是 .parent 元素之下的 a 元素的事件代理到 $('.parent') 之上,只要在這個(gè)元素上有點(diǎn)擊事件,就會(huì)自動(dòng)尋找到 .parent 元素下的 a 元素,然后響應(yīng)事件;
$.delegate: 基本用法 : $('.parent').delegate('a', 'click', function () { console.log('click event on tag a'); }) ,同上,并且還有相對(duì)應(yīng)的 $.delegate 來刪除代理的事件;
$.live: 基本使用方法 : $('a', $('.parent')).live('click', function () { console.log('click event on tag a'); }) ,同上,然而如果沒有傳入父層元素 $(.parent) ,那事件會(huì)默認(rèn)委托到 $(document) 上; ( 已廢除 )
實(shí)現(xiàn)功能
基本實(shí)現(xiàn)
比如我們有這樣的一個(gè) HTML 片段:
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
// ...... 代表中間還有未知數(shù)個(gè) li
我們來實(shí)現(xiàn)把 #list 下的 li 元素的事件代理委托到它的父層元素也就是 #list 上:
// 給父層元素綁定事件
document.getElementById('list').addEventListener('click', function (e) {
// 兼容性處理
var event = e || window.event;
var target = event.target || event.srcElement;
// 判斷是否匹配目標(biāo)元素
if (target.nodeName.toLocaleLowerCase === 'li') {
console.log('the content is: ', target.innerHTML);
}
});
在上述代碼中, target 元素則是在 #list 元素之下具體被點(diǎn)擊的元素,然后通過判斷 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>
// ...... 代表中間還有未知數(shù)個(gè) li
這里,我們想把 #list 元素下的 li 元素(并且它的 class 為 class-1 )的點(diǎn)擊事件委托代理到 #list 之上;
如果通過上述的方法我們還需要在 `if (target.nodeName.toLocaleLowerCase === 'li')` 判斷之中在加入一個(gè)判斷 `target.nodeName.className === 'class-1'` ;
但是如果想像 CSS 選擇其般做更加靈活的匹配的話,上面的判斷未免就太多了,并且很難做到靈活性,這里可以使用 Element.matches API 來匹配;
Element.matches API 的基本使用方法 : Element.matches(selectorString) , selectorString 既是 CSS 那樣的選擇器規(guī)則,比如本例中可以使用 target.matches('li.class-1') ,他會(huì)返回一個(gè)布爾值,如果 target 元素是標(biāo)簽 li 并且它的類是 class-1 ,那么就會(huì)返回 true ,否則返回 false ;
當(dāng)然它的兼容性還有一些問題,需要 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 之后就可以來實(shí)現(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)對(duì)更多場(chǎng)景上我們可以把事件代理的功能封裝成一個(gè)公用函數(shù),這樣就可以重復(fù)利用了。
結(jié)合上面的例子來實(shí)現(xiàn)一個(gè)函數(shù) eventDelegate ,它接受四個(gè)參數(shù):
[String] 一個(gè)選擇器字符串用于過濾需要實(shí)現(xiàn)代理的父層元素,既事件需要被真正綁定之上;
[String] 一個(gè)選擇器字符串用于過濾觸發(fā)事件的選擇器元素的后代,既我們需要被代理事件的元素;
[String] 一個(gè)或多個(gè)用空格分隔的事件類型和可選的命名空間,如 click 或 keydown.click ;
[Function] 需要代理事件響應(yīng)的函數(shù);
這里就有幾個(gè)關(guān)鍵點(diǎn):
對(duì)于父層代理的元素可能有多個(gè),需要一一綁定事件;
對(duì)于綁定的事件類型可能有多個(gè),需要一一綁定事件;
在處理匹配被代理的元素之中需要考慮到兼容性問題;
在執(zhí)行所綁定的函數(shù)的時(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));
}
}
// 如果有多個(gè)事件的話需要全部一一綁定事件
events.split('.').forEach(function (evt) {
// 多個(gè)父層元素的話也需要一一綁定
Array.prototype.slice.call(document.querySelectorAll(parentSelector)).forEach(function ($p) {
$p.addEventListener(evt, triFunction);
});
});
}
優(yōu)化
當(dāng)被代理的元素不是目標(biāo)元素的時(shí)候,既選擇器 targetSelector 所指向的元素不是 event.target (事件目標(biāo)階段指向的元素)的時(shí)候,這時(shí)候就需要層層遍歷 event.target 的 parentNode 去匹配 targetSelector 了,直到 parentSelector 。
比如:
<ul id="list">
<li><span>item 1</span></li>
<li><span>item 2</span></li>
</ul>
還是把 li 的事件代理到 #list 之上,但這時(shí)候會(huì)發(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;
// 獲取到目標(biāo)階段指向的元素
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;
}
}
// 如果有多個(gè)事件的話需要全部一一綁定事件
events.split('.').forEach(function (evt) {
// 多個(gè)父層元素的話也需要一一綁定
Array.prototype.slice.call(document.querySelectorAll(parentSelector)).forEach(function ($p) {
$p.addEventListener(evt, triFunction);
});
});
}
使用函數(shù):
eventDelegate('#list', 'li', 'click', function () { console.log(this); });
點(diǎn)擊后可以看到 console 出了 `#list li` 元素對(duì)象;
局限性
當(dāng)然,事件委托也是有一定局限性的;
比如 focus 、 blur 之類的事件本身沒有事件冒泡機(jī)制,所以無法委托;
mousemove 、 mouseout 這樣的事件,雖然有事件冒泡,但是只能不斷通過位置去計(jì)算定位,對(duì)性能消耗高,因此也是不適合于事件委托的;
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。