Change before you have to.

Event binding and event delegation

2017-02-04

到了,叫我。

瀏覽器的事件處理很像我們日常生活中會發生的事,針對某些目標,一旦有情況發生,就執行相對應的程序。
就像買票去墾丁,我們(對象目標)一旦到達墾丁(情況發生),我們就會下車(執行程序)。

Event Binding

針對瀏覽器,使用者觸發的行為( 例如 mousemove 或 click ) 或是系統自行觸發的行為 ( 例如 load ),我們稱這些行為為事件(event),並且可以對這些事件註冊事件處理器(event handler),當監聽的事件一旦發生,就執行對應的 event handler。

以下我們來分別使用 javascript 及 jQuery 寫出一個範例,當使用者在螢幕按下滑鼠時,會在瀏覽器發開者工具的頁面看見輸出 hello world ,以下為分別的角色:

1
2
3
4
5
6
7
8
// javascript
function sayHello() {
console.log('hello world');
}
window.addEventListener('click', sayHello);
1
2
3
4
5
6
7
8
// jQuery
function sayHello() {
console.log('hello world');
}
$(window).on('click', sayHello);

將此觀念擴展並實作後,我們可以指定目標,針對目標設定需要監聽事件,並為這些事件註冊事件處理器。一旦目標所監聽的事件發生,就執行事件處理器。

Event Capturing and Bubbling

在談論 event delegation 之前,我們先來了解事件( event )是如何在 DOM tree 傳遞。
以下面的 dom 為例子,當我們用滑鼠按了 click me 的按鈕,click 事件便開始在 dom 之間傳遞,直到事件完成 1 個 cycle,結束事件的傳遞。
事實上,事件的傳遞可分為 3 個部分:

整體來看的概念如以下所示:

1
2
3
4
5
6
7
8
9
10
<-- html 結構 -->
<div id="wrapper1">
<div id="wrapper2">
<div id="wrapper3">
<button id="target">click me</button>
</div>
</div>
</div>

javascript 預設的事件處理為 event bubbling,原因在此不贅述,簡單的說,事件的傳遞在 capturing 階段不執行任何的 event handler,直到事件傳遞到 target 及 bubbling 階段,才針對監聽的事件,執行 event handler。

根據以下的程式碼範例,我們可以看見事件傳遞階段會依序發生的事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// javascript
var wrapper1Node = document.getElementById('wrapper1');
var wrapper2Node = document.getElementById('wrapper2');
var wrapper3Node = document.getElementById('wrapper3');
var targetNode = document.getElementById('target');
wrapper1Node.addEventListener('click', function() {
console.log('this is wrapper1');
})
wrapper2Node.addEventListener('click', function() {
console.log('this is wrapper2');
})
wrapper3Node.addEventListener('click', function() {
console.log('this is wrapper3');
})
targetNode.addEventListener('click', function() {
console.log('this is target');
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//jQuery
$('#wrapper1').on('click', function(){
console.log('this is wrapper1');
});
$('#wrapper2').on('click', function(){
console.log('this is wrapper2');
});
$('#wrapper3').on('click', function(){
console.log('this is wrapper3');
});
$('#target').on('click', function(){
console.log('this is target');
});

Event Delegation

Event Delegation 即事件代理,在討論實作之前,我們先針對以下的需求,寫出相對應的程式碼。

需求: 當使用按了 list 元素,在 browser dev tool 輸出 list 中的文字

1
2
3
4
5
6
7
8
<ul id="bookList">
<li class="bookListItem">book 1</li>
<li class="bookListItem">book 2</li>
<li class="bookListItem">book 3</li>
<li class="bookListItem">book 4</li>
<li class="bookListItem">book 5</li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
// javascript
var listNodes = document.querySelectorAll('.bookListItem');
listNodes = Array.from(listNodes); // convert to array
listNodes.forEach(function(listNode) {
listNode.addEventListener('click', function(e) {
console.log(e.target.innerHTML);
});
});
1
2
3
4
5
6
// jQuery
$('.bookListItem').on('click', function() {
console.log($(this).html());
})

由上述的程式碼,我們可以看出針對每個 list node 設定監聽 click 的事件,當事件發生時,會執行 event hanlder 的內容,即在 browser dev tool 輸出 list 中的文字。

但是在某些常見的情況,上述的解法會有效能問題或無法實作的情況:

上述的情況可以由 evnet delegation 幫我們解決,由於 event 具有 bubbling 的特性,因此針對槽狀的 html 結構,並不需要一一為內部 dom node 綁定事件,而是可以只需在外圍的 dom node 綁定事件,一旦事件觸發時,直到事件 bubble 至外圍的 dom node時,才執行 event hanlder。
以下為改善之前的例子而使用 event delegatin 的程式碼。

1
2
3
4
5
6
7
8
9
10
// javascript
var bookListNode = document.querySelector('#bookList');
bookListNode.addEventListener('click', function(e) {
if(e.target && e.target.className === 'bookListItem') {
console.log(e.target.innerHTML);
}
});
1
2
3
4
5
6
// jQuery
$('#bookList').on('click', '.bookListItem', function() {
console.log($(this).html());
})

Blog comments powered by Disqus