事件委託是利用事件冒泡原理,管理某一型別的所有事件,利用父元素來代表子元素的某一型別事件的處理方式。
接下來我們通過兩種比較常見的場景來進行分析,一種是已有元素的事件繫結,另一種是新建立元素的事件繫結。
已有元素的事件繫結
場景:假如頁面上有一個 ul 標籤,裡面包含 1000 個 li 子標籤,我們需要在單擊每個 li 時,輸出 li 中的文字內容。
遇到這樣的場景時,很多人的第一想法就是給每個 li 標籤繫結一個 click 事件,在 click 事件中輸出 li 標籤的文字內容,以下是一些簡單易懂的實現方法。
HTML 程式碼
HTML 程式碼很簡單,就是一個包含很多 li 標籤的 ul 標籤,後面過多的程式碼使用省略號代替。
<ul>
<li>文字1</li>
<li>文字2</li>
<li>文字3</li>
<li>文字4</li>
<li>文字5</li>
<li>文字6</li>
<li>文字7</li>
<li>文字8</li>
<li>文字9</li>
...
</ul>
JavaScript程式碼
在獲取所有的 li 標籤後,對其進行遍歷,在遍歷時新增 click 事件處理程式。
<script>
// 1.獲取所有的li標籤
var children = document.querySelectorAll('li');
// 2.遍歷新增click事件處理程式
for (var i = 0; i < children.length; i++) {
children[i].addEventListener('click', function () {
console.log(this.innerText);
});
}
</script>
當我們單擊 li 標籤時,會對應地輸出 li 標籤上的內容,如下所示:
文字1
文字6
文字9
文字7
採用上述的方法對瀏覽器的效能是一個很大的挑戰,主要包含以下兩方面原因:
假如有 1000 個 li 元素,則需要繫結 1000 個事件處理程式,而事件處理程式需要不斷地與 DOM 節點進行互動,因此引起瀏覽器重繪和重排的次數也會增多,從而會延長頁面互動時間。
在 JavaScript 中,一個事件處理程式其實就是一個函數物件,會佔用一定的記憶體空間。假如頁面有 10000 個 li 標籤,則會有 10000 個函數物件,佔用的記憶體空間會急劇上升,從而影響瀏覽器的效能。
那麼遇到這個問題時,有什麼好的解決辦法呢?答案就是利用事件委託機制。
事件委託機制的主要思想是將事件繫結至父元素上,然後利用事件冒泡原理,當事件進入冒泡階段時,通過繫結在父元素上的事件物件來判斷當前事件流正在進行的元素。如果和期望的元素相同,則執行相應的事件程式碼。
根據以上的分析,我們可以按步驟依次得到以下程式碼:
// 1.獲取父元素
var parent = document.querySelector('ul');
// 2.父元素繫結事件
parent.addEventListener('click', function (event) {
// 3.獲取事件物件
var event = EventUtil.getEvent(event);
// 4.獲取目標元素
var target = EventUtil.getTarget(event);
// 5.判斷當前事件流所處的元素
if (target.nodeName.toLowerCase() === 'li') {
// 6.與目標元素相同,做對應的處理
console.log(target.innerText);
}
});
執行上面的程式碼,當我們單擊 li 標籤時,會得到與前面方法相同的輸出。
通過上面的程式碼可以看出,事件是繫結在父元素 ul 上的,不管子元素 li 有多少個,也不會影響到頁面中事件處理程式的個數,因此可以極大地提高瀏覽器的效能。
新建立元素的事件繫結
場景:假如頁面上有一個 ul 標籤,裡面包含 9 個 li 子標籤,我們需要在單擊每個 li 時,輸出 li 中的文字內容;在頁面上有一個 button 按鈕,單擊 button 按鈕會建立一個新的 li 元素,單擊新建立的 li 元素,輸出它的文字內容。
根據上面的場景描述,我們可以通過以下兩種方法來實現:
手動繫結方法
首先是和已有元素的事件繫結中相同的程式碼,由於邏輯是相同的,這裡就不贅述,直接給出程式碼。
<ul>
<li>文字1</li>
<li>文字2</li>
<li>文字3</li>
<li>文字4</li>
<li>文字5</li>
<li>文字6</li>
<li>文字7</li>
<li>文字8</li>
<li>文字9</li>
</ul>
// 1.獲取所有的li標籤
var children = document.querySelectorAll('li');
// 2.遍歷新增click事件處理程式
for (var i = 0; i < children.length; i++) {
children[i].addEventListener('click', function () {
console.log(this.innerText);
});
}
然後在頁面上新增一個 button 按鈕,用於新增一個 li 元素。
<button id="add">新增</button>
var ul = document.querySelector('ul');
var add = document.querySelector('#add');
add.addEventListener('click', function () {
// 建立新的li元素
var newLi = document.createElement('li');
var newText = document.createTextNode('文字10');
newLi.appendChild(newText);
// 新增至父元素ul中
ul.appendChild(newLi);
});
當我們單擊新增按鈕時,會發現頁面上新增了一個內容為“文字10”的 li 元素。
當我們單擊這個 li 元素時,會在控制檯輸出“文字10”嗎?
我們在瀏覽器中驗證後會發現,控制檯中沒有輸出任何內容。這是為什麼呢?
因為我們通過 querySelectorAll() 函數獲取到的 li 元素雖然會實時感知到數量的變化,但並不會實時增加對事件的繫結。如果需要新元素也具有相同的事件,則需要手動呼叫事件繫結的程式碼。
解決方案如下:
-
將遍歷新增 click 事件處理程式程式碼封裝成一個函數。
// 遍歷新增click事件處理程式
function bindEvent() {
for (var i = 0; i < children.length; i++) {
children[i].addEventListener('click', function () {
console.log(this.innerText);
});
}
}
add.addEventListener('click', function () {
var newLi = document.createElement('li');
var newText = document.createTextNode('文字10');
newLi.appendChild(newText);
ul.appendChild(newLi);
// 重新新增事件處理程式
bindEvent();
});
但是,通過上面的分析我們發現,每次在新增一個元素後都需要手動繫結事件處理程式,這樣的操作是很煩瑣的,而且隨著繫結的事件處理程式越來越多,效能也將受到影響。
那麼,我們有沒有什麼更好的方法呢?答案就是使用事件委託機制。
事件委託方法
使用事件委託機制,我們可以更加方便快捷地實現新建立元素的事件繫結。由於事件委託機制是利用的事件冒泡機制,即使在元素自身沒有繫結事件的情況下,事件仍然會冒泡到父元素中,因此對於新增的元素,只要處理事件流就可以觸發其事件。
針對上述問題的描述,我們需要做的就是使用事件委託機制編寫程式碼。
<script>
// 1.獲取父元素
var parent = document.querySelector('ul');
// 2.父元素繫結事件
parent.addEventListener('click', function (event) {
// 3.獲取事件物件
var event = EventUtil.getEvent(event);
// 4.獲取目標元素
var target = EventUtil.getTarget(event);
// 5.判斷當前事件流所處的元素
if (target.nodeName.toLowerCase() === 'li') {
// 6.與目標元素相同,做對應的處理
console.log(target.innerText);
}
});
</script>
新增按鈕的事件不變,和手動繫結方法中的一樣,這裡就不贅述。
當我們在瀏覽器中執行可以發現,新增的 li 元素在單擊後,會在控制檯輸出“文字10”,這就代表使用事件委託機制方便快捷地解決了這個問題。