您好,登錄后才能下訂單哦!
好程序員 分享 JavaScript 事件委托代理和函數封裝詳解 , JavaScript 高級程序設計上講:事件委托就是利用事件冒泡,只指定一個事件處理程序,就可以管理某一類型的所有事件。下面我們一起看看事件代理相關介紹。
當用事件委托的時候,根本就不需要去遍歷元素的子節點,只需要給父級元素添加事件就好了,其他的都是在 js 里面的執行,這樣可以大大的減少 dom 操作,這才是事件委托的精髓所在。
基本概念
事件委托,通俗地來講,就是把一個元素響應事件( click 、 keydown...... )的函數委托到另一個元素;
一般來講,會把一個或者一組元素的事件委托到它的父層或者更外層元素上,真正綁定事件的是外層元素,當事件響應到需要綁定的元素上時,會通過事件冒泡機制從而觸發它的外層元素的綁定事件上,然后在外層元素上去執行函數。
舉個例子,比如一個宿舍的同學同時快遞到了,一種方法就是他們都傻傻地一個個去領取,還有一種方法就是把這件事情委托給宿舍長,讓一個人出去拿好所有快遞,然后再根據收件人一一分發給每個宿舍同學;
在這里,取快遞就是一個事件,每個同學指的是需要響應事件的 DOM 元素,而出去統一領取快遞的宿舍長就是代理的元素,所以真正綁定事件的是這個元素,按照收件人分發快遞的過程就是在事件執行中,需要判斷當前響應的事件應該匹配到被代理元素中的哪一個或者哪幾個。
事件冒泡
前面提到 DOM 中事件委托的實現是利用事件冒泡的機制,那么事件冒泡是什么呢?
在 document.addEventListener 的時候我們可以設置事件模型:事件冒泡、事件捕獲,一般來說都是用事件冒泡的模型;
如上圖所示,事件模型是指分為三個階段:
捕獲階段:在事件冒泡的模型中,捕獲階段不會響應任何事件;
目標階段:目標階段就是指事件響應到觸發事件的最底層元素上;
冒泡階段:冒泡階段就是事件的觸發響應會從最底層目標一層層地向外到最外層(根節點),事件代理即是利用事件冒泡的機制把里層所需要響應的事件綁定到外層; ### 事件
委托的優點
1. 減少內存消耗
試想一下,若果我們有一個列表,列表之中有大量的列表項,我們需要在點擊列表項的時候響應一個事件;
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
// ...... 代表中間還有未知數個 li
如果給每個列表項一一都綁定一個函數,那對于內存消耗是非常大的,效率上需要消耗很多性能;
因此,比較好的方法就是把這個點擊事件綁定到他的父層,也就是 `ul` 上,然后在執行事件的時候再去匹配判斷目標元素;
所以事件委托可以減少大量的內存消耗,節約效率。
2. 動態綁定事件
比如上述的例子中列表項就幾個,我們給每個列表項都綁定了事件;
在很多時候,我們需要通過 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 這樣的事件,雖然有事件冒泡,但是只能不斷通過位置去計算定位,對性能消耗高,因此也是不適合于事件委托的;
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。