[WebAPIs] Event, EventListener and Event Target
keywords: event listener
, event target
, custom event
, event delegation
, 自定義事件
- Event API @ MDN Web API
- Introduction to Events @ MDN - Learn JavaScript
- 事件 (Event) 的註冊、觸發與傳遞 @ Medium by realDennis
EventListener
// target.addEventListener(type, listener[, options]);
myElement.addEventListener('click', function () {
// do anything you want once
}, {
useCapture: false, // 預設 false, true 會註冊 capture 事件;false 會註冊 bubbling 事件
once: true,
passive: false, // true 的話該 function 永遠不會呼叫 preventDefault() 即使有自己寫在內)
}
- EventTarget @ MDN - Web APIs
取得正確的 target 元素
e.target; // 引發該事件的元素(可能不是被綁定監聽的事件)
e.currentTarget; // 被監聽事件的元素, 等同於 this 拿到的對象
addEventListener with parameter
如果監聽的元素需要在 callback function 中代入參數,則需要另外包一層函數:
const btn = document.querySelector('#btn');
btn.addEventListener('click', function (e) {
insertText('foobar', e);
});
function insertText(params, e) {
console.log('eventParams', e);
console.log('params', params);
}
或者:
const btn = document.querySelector('#btn');
btn.addEventListener('click', clickHandler('foobar'));
function clickHandler(parameters) {
return function (e) {
console.log('I can get the event: ', e.target.textContent);
console.log('I can get the parameter: ', parameters);
};
}
將參數傳送到 event handler 中 @ JSFiddle
Passive vs. non-passive mode
- **Passive Mode(passive: true)**時,畫面的轉譯和 event handler 中處理的事件會「類似」非同步,畫面的轉譯不會被 event handler 中的 JavaScript 給阻塞。
- **Non-passive Mode(passive: false)**時,畫面的轉譯會被 event handler 中處理的事件所阻塞,因此操作起來會類似「同步」的感覺,容易造成使用者體驗上的卡頓。
See the Pen Passive Mode of Event Listener by PJCHEN (@PJCHENder) on CodePen.
addEventListener for ONCE
使用 {once: true}
可以讓該事件只被促發一次:
myElement.addEventListener(
'transitionend',
(event) => {
// do something
},
{ once: true },
);
One-off Event Listeners: Micro Tip #28 - Supercharged @ Google Chrome Developers
removeEventListener
EventTarget.removeEventListener();
removeEventListener @ MDN - Web APIs
[BAD] Add EventListener Recursively
示範:CodePen @ PJCHENder
Identical Event Listeners & Memory Issue
Multiple Identical Event Listeners @ MDN Memory Issues @ MDN
var i;
var els = document.getElementsByTagName('*');
// Case 1:使用匿名函式當用 EventListener
for (i = 0; i < els.length; i++) {
els[i].addEventListener(
'click',
function (e) {
/*do something*/
},
false,
);
}
// Case 2:有給 EventListener 一個參照的函式
function processEvent(e) {
/* do something */
}
for (i = 0; i < els.length; i++) {
els[i].addEventListener('click', processEvent, false);
}
Case 1:但是當透過匿名函式作為 EventListeners
時,每一次被執行的匿名函式本質上是不一樣的(即使函式內的程式碼完全相同),因此該 Listeners 會被多次呼叫與執行,同時因為是匿名函式的緣故,無法使用 removeEventListener()
這個方法。
Case 2:當相同的 EventListener
被註冊到相同的 EventTarget
時(options
也相同),重 複內容會被取消,因此該 EventListener
不會被重複呼叫執行,也因此不需要手動透過 removeEventListener()
移除該方法,也會有較小的記憶體消耗。
// 「錯誤」使用 EventListener
// 這裡為了示範,故意把 [i] 寫成 [j],將導致所有的事件都註冊到「同一」元素上
// Case 3
for (var i = 0, j = 0; i < els.length; i++) {
/* do lots of stuff with j */
els[j].addEventListener(
'click',
(processEvent = function (e) {
/*do something*/
}),
false,
);
}
// Case 4
for (var i = 0, j = 0; i < els.length; i++) {
/* do lots of stuff with j */
function processEvent(e) {
/*do something*/
}
els[j].addEventListener('click', processEvent, false);
}
實際上,重要的不只是把 EventListener 給予一個函式作為參照(function reference),而是要給予一個靜態的函式作為參照(STATIC function reference)。在上面這兩個雖然都有給 EventListener 一個實名函式最為參照,但卻在每次迴圈疊代時都重新定義了該函式,因此實際上這個函式並不是靜態的參照。
❌ **Case 3:**在這個例子中,匿名函式的參照在每次迴圈疊代時都被重新指派。
❌ **Case 4:**重複定義該函式,變成這個函式每次都是參照的新的位置,因此也不是靜態的。
事件委派(Event Delegation)
- Event Delegate @ DWB
- element.matches(selector) @ MDN > Web technology for developers
使用 e.target
搭配 e.target.matches
來篩選事件中的元素,即可達到 event delegation 的效果:
// 事件雖然綁在 <ul> 上,但是行為是對被點擊的對象(e.target)
ul.addEventListener('click', hide, false);
function hide(e) {
// Make sure it's not the !DOCTYPE object
if (!'matches' in event.target) return;
if (e.target && e.target.matches('li')) {
e.target.style.visibility = 'hidden';
console.log('List item ', e.target, ' was clicked!');
}
}
e.target
指稱的是被觸發事件的元素<li>
>e.currentTarget
指稱到綁定事件的元素<ul>
自定義事件(Custom EventListener)
基本使用
在 JavaScript 中我們可以透過 new Event('<event name>')
自定義事件,透過 EventTarget.dispatchEvent()
來射出事件 :
// 建立事件
var myCustomEvent = new Event('myCustomEventTrigger', { bubbles: true, cancelable: true, detail: thingsPastIntoEvent });
// 觸發事件
elem.dispatchEvent(myCustomEvent);
// 監聽事件
elem.addEventListener('myCustomEventTrigger', function (e) { ... }, false);
使用 new Event()
時:
- bubbles:預設不會自動冒泡(bubbles),若需事件可以透過冒泡向外傳遞,可以代入選項
bubbles: true
- cancelable:若中間需要停止事件傳遞,可以代入參數
cancelable: true
- detail:使用
detail
可以在把需要代入到 event handler 的資料放到裡面
使用範例
// 建立事件
var myCustomEvent = new Event('myCustomEventTrigger');
setTimeout(function () {
// 觸發事件
document.dispatchEvent(myCustomEvent);
}, 1500);
// 監聽事件
document.addEventListener(
'myCustomEventTrigger',
function (e) {
console.log('my custom event is triggered...');
},
false,
);
增加自訂義的資料
範例程式碼 @ JSFiddle
若要在事件的物件中追加其他資料,可以使用 CustomEvent
API。它有 detail 屬性,可以用來傳送自訂的資料:
let event = new CustomEvent('build', { detail: elem.dataset.time });
❗ 注意:這裡是
new CustomEvent()
而不是new Event()
。
接著就可以在事件觸發的 event 物件中使用此 detail
:
function eventHandler(e) {
console.log('The time is: ' + e.detail);
}
detail
是關鍵字,不能換成其他的。- 如果想要在每次
dispatchEvent
時傳送不同的detail
,就在dispatchEvent
前去new CustomEvent
(可重複new
它)。
Sample Code
See the Pen [JS] Custom Events by PJCHEN (@PJCHENder) on CodePen.
參考
- Custom Event: event constructor @ MDN - WebAPIs
- Creating and triggering events @ MDN - Guides
- Deprecated: Document.createEvent() @ MDN - WebAPIs
- Dive deeper into the Event object. Creating and using custom events @ wango.io