跳至主要内容

[WebAPIs] Event, EventListener and Event Target

keywords: event listener, event target, custom event, event delegation, 自定義事件

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() 即使有自己寫在內)
}

取得正確的 target 元素

e.target; // 引發該事件的元素(可能不是被綁定監聽的事件)
e.currentTarget; // 被監聽事件的元素, 等同於 this 拿到的對象

A Quick Look at e.target and e.currentTarget. And Bubbling

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 中處理的事件所阻塞,因此操作起來會類似「同步」的感覺,容易造成使用者體驗上的卡頓。

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)

使用 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.

參考