# 事件機制的原理
當對網頁進行操作動作,如點擊等,就可以被稱做「事件」(Event),而負責處理事件的程式通常被稱為「事件處理者」(Event Handler)。
# 事件流程
事件流程 (Event Flow) 指的是 網頁元素接受事件的順序。
事件流程可以分成兩種機制:
- 事件冒泡 (Event Bubbling)
- 事件捕獲 (Event Capturing)
# 事件冒泡
事件冒泡指的是 從啟動事件的元素節點開始,逐漸往上遞傳,直到整個網頁的根節點,也就是 document
。
<html>
<body>
<div>click</div>
</body>
</html>
2
3
4
5
當我們點擊了 <div>click</div>
元素,那麼在事件冒泡的機制下,觸發事件的順序是:
<div>click</div>
<body>
<html>
document
# 事件捕獲
事件捕獲的機制是由上往下傳遞,與事件冒泡相反。
以上面的例子,當點擊 <div>click</div>
元素,觸發事件的順序會是:
document
<html>
<body>
<div>click</div>
# 事件的依賴機制
既然事件傳遞順序有兩種機制,那麼在做點擊的行為時,兩種機制都會執行。一般會由上而下依序觸發 (capture phase),接著執行一路往上傳至 document
(bubble phase),整個事件流程就到此結束。
在綁定事件的時後,通常會使用 addEventListener
的方法,而它的第三個參數為 boolean 值,分別代表 捕獲 (true) 和 冒泡 (false) 機制,如果不指定則預設為 冒泡。
注意操作的目標
<div id="parent">
父元素
<div id="child">子元素</div>
</div>
2
3
4
點選
#child
時,不論程式碼的先後順序,會是:#parent
的capturing
#child
的capturing
#child
的bubbling
#parent
的bubbling
點選
#parent
時,capturing
或bubbling
誰先誰後呢?依程式碼的順序而定。若是
bubbling
在capturing
的程式碼前面,就會先執行bubbling
再來是capturing
。
# 事件的註冊綁定
# on-event 處理器 (HTML 屬性)
對 HTML 元素來說,只要支援某個事件的觸發,就可以透過 on + 事件名稱
的屬性來註冊事件。
<button id="btn" onclick="console.log('HI');">Click</button>
注意
基於程式碼的使用性與維護性考量,現在已經不建議用此方式來綁定事件,可以操考 維基百科:非侵入式 JavaScript (opens new window)。
# on-event 處理器 (非 HTML 屬性)
若是實體元素也可透過 DOM API 取得 DOM 物件後,再透過 on-event 處理器來處理事件。
const btn = document.getElementById('btn');
btn.onclick = function() {
console.log('HI');
};
2
3
4
5
想解除事件的話,則重新指定 on-event 處理器為 null
即可。
btn.onclick = null;
# 事件監聽 EventTarget.addEventListener()
on-event
對應的 function
指的是事件處理器,而 addEventListener()
代表的是事件監聽器。
使用 addEventListener
的方式來註冊事件的好處是可以重複指定多個「處理器」給同一個元素的同一個事件。
const btn = document.getElementById('btn');
btn.addEventListener('click', function() {
console.log('HI');
});
btn.addEventListener('click', function() {
console.log('HELLO');
});
// 點擊後會出現,'HI' 和 'HELLO'
2
3
4
5
6
7
8
9
10
11
另外也可以設定事件監聽器自訂的屬性:
- once - 該監聽器是否只會觸發一次
- passive - 是否「不能」執行
event.preventDefault()
,常用在提升監聽器的效能如scroll
- capture - 事件的捕獲機制,
true
代表「捕獲機制」,false
則為「冒泡機制」
提醒
可以透過 Dev Tool 的 getEventListeners(element)
,觀察指定物件的事件監聽器註冊內容。
getEventListeners($('.cat'));
// {
// "click": [
// {
// "listener": onclick(event)
// "useCapture": false,
// "passive": false,
// "once": false,
// "type": "click"
// },
// {
// "listener": log()
// "useCapture": true,
// "passive": true,
// "once": true,
// "type": "click"
// }
// ]
// }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
若是要解除事件的註冊,則是透過 removeEventListener()
來取消。
注意
由於 addEventListener()
可以同時針對某個事件綁定多個 handler,所以透過 removeEventListener()
解除事件的時需要注意:
- 第一個參數的事件名稱需相同
- 第二個參數的 handler 必須要與先前在
addEventListener()
綁定的 handler 是同一個 實體 - 第三個參數需填入相同的設定
const btn = document.getElementById('btn');
const clickHandler = function() {
console.log('HI');
};
const options: {
capture: true,
passive: true,
once: false
}
btn.addEventListener('click', clickHandler, options);
btn.removeEventListener('click', clickHandler, options);
2
3
4
5
6
7
8
9
10
11
12
# 阻擋預設行為
可以在「事件處理器」的實體中,透過「事件物件」提供的 event.preventDefault()
方法阻擋某些元素的預設行為。
但要注意的是,event.preventDefault
並不會阻止事件向上傳遞 (bubbling)。
另外,在 HTML 的 on-event 中使用 return false
,也會有 event.preventDefault
的效果。
<a href="#" onclick="return false;"></a>
# 阻擋事件冒泡傳遞
如果我們想要阻擋事件向上冒泡傳遞,那麼就可以利用 event object 提供的方法 event.stopPropagation()
。
注意
stopPropagation()
需要實做的位置為 子層的目標 上,而非父層上。
# 在事件中找到自己
在 Event Handler 的 function 裡頭,若是想要對「觸發事件的元素」做某些事時,可以透過 this
、e.target
、e.currentTarget
找到觸發事件的元素,但其中會有一些差異,先來看它們各自的定義:
this
- 當時處理該事件的事件監聽器所 註冊的 DOM 物件e.target
- 指向 觸發事件 的 DOM 物件e.currentTarget
- 和this
相同
<label class="lbl">
Label <input type="checkbox" name="chkbox" id="chkbox" />
</label>
2
3
const lbl = document.querySelector('.lbl');
const chkbox = document.querySelector('#chkbox');
lbl.addEventListener(
'click',
function(e) {
console.log(this.tagName, 1);
console.log(e.target);
console.log(e.currentTarget);
},
false
);
chkbox.addEventListener(
'click',
function(e) {
console.log(this.tagName, 2);
console.log(e.target);
console.log(e.currentTarget);
},
false
);
// 執行結果
// LABEL 1
// <label class="lbl">…</label>
// <label class="lbl">…</label>
// INPUT 2
// <input type="checkbox" name="chkbox" id="chkbox">
// <input type="checkbox" name="chkbox" id="chkbox">
// LABEL 1
// <input type="checkbox" name="chkbox" id="chkbox">
// <label class="lbl">…</label>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
上面的範例,因為 checkbox
沒有設定 e.stopPropagation()
,所以會受到事件冒泡的影響,又再度把 click
事件傳遞給 label
,導致最後 label
還會再執行一次 click
事件。
另外,可以注意到最後一次的 click
事件,觸發事件的對象為 checkbox
,所以 e.target
會為 checkbox
。
提醒
如果在不考慮事件傳遞的情況下,this
實質上就等同於 e.target
了。
# 介面相關事件
介面相關的事件不一定會與使用者對 DOM 的操作有關係,反而大多數與 window
物件比較相關。
load
事件 - 註冊在window
物件上,指的是網頁資源 (包括 CSS、JS、圖片等) 全數載入完畢後觸發unload
、beforeunload
事件 - 與load
事件相反,unload
與beforeunload
事件分別會在離開頁面或重新整理時觸發,而beforeunload
會跳出對話框詢問使用者是否要離開目前頁面error
事件 -error
事件會在document
或是圖片載入錯誤時觸發,另外,由於維護性的考量,大多事件是使用「非侵入式 JavaScript」的寫法,不過只有error
事件最適合以on-event
的寫法來處理<img src="image.jpg" onerror="this.src = 'default.jpg'" />
1resize
事件 - 當瀏覽器 (window) 或指定元素 (element) 的「尺寸變更」時觸發scroll
事件 - scroll 事件:當瀏覽器 (window) 或指定元素 (element) 的「捲軸被拉動」時觸發DOMContentLoaded
事件 - 在 DOM 結構被完整的讀取跟解析後,就會被觸發,不需等待外部資源讀取完成。以
<script>
放在<head>
中,並試圖修改 DOM 節點的內容時:// 有錯誤:Cannot set property 'textContent' of null document.querySelector('h1').textContent = 'Welcome'; // 執行成功 document.addEventListener('DOMContentLoaded', function() { document.querySelector('h1').textContent = 'Welcome'; });
1
2
3
4
5
6
7
# Composition Event (組成事件)
Composition Event 其實指的是 compositionstart
、compositionend
,以及 compositionupdate
這三個事件。
常見的情況為透過關鍵字來做到搜尋,但在輸入中文時,需要透過注音之類的輸入法來做拼字,如果不想利用「注音符號」和「拼音文字」做到即時搜尋,便可藉由 composition
和 input
事件的結合,達到效果。
const input = document.querySelector('input');
let inputLock = false;
input.addEventListener('compositionstart', (e) => {
inputLock = true;
console.log('compositionstart');
});
input.addEventListener('compositionupdate', (e) => {
console.log('compositionupdate');
});
input.addEventListener('compositionend', (e) => {
inputLock = false;
console.log('compositionend');
console.log(e.data);
});
input.addEventListener('input', (e) => {
if (!inputLock) {
console.log('input');
console.log(e.data);
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
上面的範例可以看到,如果要確認使用者輸入完成並送出文字時,就可以透過 compositionend
來做最後確認。
# 自訂事件
自訂事件可以用 Event constructor
建立,同樣透過 addEventListener
去監聽,由 dispatchEvent
決定觸發的時機。
const buildEvent = new Event('build');
h1.addEventListener('build', function(e) {
console.log(e.type);
});
h1.dispatchEvent(buildEvent);
// 'build'
2
3
4
5
6
7
8
9
如果想要在自訂事件內增加更多資料,則可以改用 CustomEvent
:
<h1 data-time="2020/11/06">Hello World</h1>
const buildEvent = new CustomEvent('build', { detail: h1.dataset.time });
在「事件物件」中,就會看到 detail
屬性,它的值就為 2020/11/06
。