JavaScript 事件傳播:冒泡與捕獲機制完整說明
事件傳播 = 事件流,就是描述頁面中接受事件的順序。大致上,可以分成兩種機制:
- 冒泡(Bubbling)。
- 捕獲(Capture)。
小提醒
一次點擊實際上會跑完三個階段:捕獲階段(由外而內)→ 目標階段(到達被點的元素)→ 冒泡階段(由內而外)。我們平常寫的監聽器只是決定要掛在哪個階段上。
冒泡
代表從觸發事件的元素開始,向外傳遞,直到網頁的根節點為止。如下方程式碼所示,我們點擊了 <h1> 元素,事件將會層層向外傳遞。
其順序為:<h1> → <body> → <html> → document → window。
<!DOCTYPE html>
<html>
<head>
<title>JavaScript - 事件傳遞</title>
</head>
<body>
<h1>click</h1>
</body>
</html>測試以下程式碼,從最內層向外傳遞。以 console.log() 印出來的內容,
依序為:in → middle → out → body → html → document → window。
<!DOCTYPE html>
<html>
<head>
<title>JavaScript - 事件傳遞</title>
</head>
<body>
<div class="box-out" id="BoxOut">
<div class="box-middle" id="BoxMiddle">
<div class="box-in" id="BoxIn"></div>
</div>
</div>
</body>
</html>window.addEventListener('click', () => {
console.log('window');
}, false)
document.addEventListener('click', () => {
console.log('document');
}, false);
document.querySelector('html').addEventListener('click', () => {
console.log('html');
}, false);
document.body.addEventListener('click', () => {
console.log('body');
}, false);
document.querySelector('#BoxOut').addEventListener('click', () => {
console.log('out');
}, false);
document.querySelector('#BoxMiddle').addEventListener('click', () => {
console.log('middle');
}, false);
document.querySelector('#BoxIn').addEventListener('click', () => {
console.log('in');
}, false);body, html {
height: 100%;
}
.box-out {
width: 500px;
}
.box-out,
.box-middle,
.box-in {
padding: 30px;
}
.box-out {
background-color: yellowgreen;
}
.box-middle {
background-color: cadetblue;
}
.box-in {
background-color: pink;
}TIP
監聽器的第三個參數為選填,在沒有設定的狀況下,預設為 false。因此,若沒有設定或設定為 false 都是向外傳遞的冒泡機制。
捕獲
此傳遞的方式剛好與冒泡相反,也就是由外向內傳遞。以下方的程式碼為例:
其順序為:window → document → <html> → <body> → <h1>。
<!DOCTYPE html>
<html>
<head>
<title>JavaScript - 事件傳遞</title>
</head>
<body>
<h1>click</h1>
</body>
</html>測試以下程式碼,從最外層向內傳遞。以 console.log() 印出來的內容,
依序為:window → document → html → body → out → middle → in。
<!DOCTYPE html>
<html>
<head>
<title>JavaScript - 事件傳遞</title>
</head>
<body>
<div class="box-out" id="BoxOut">
<div class="box-middle" id="BoxMiddle">
<div class="box-in" id="BoxIn"></div>
</div>
</div>
</body>
</html>window.addEventListener('click', () => {
console.log('window');
}, true)
document.addEventListener('click', () => {
console.log('document');
}, true);
document.querySelector('html').addEventListener('click', () => {
console.log('html');
}, true);
document.body.addEventListener('click', () => {
console.log('body');
}, true);
document.querySelector('#BoxOut').addEventListener('click', () => {
console.log('out');
}, true);
document.querySelector('#BoxMiddle').addEventListener('click', () => {
console.log('middle');
}, true);
document.querySelector('#BoxIn').addEventListener('click', () => {
console.log('in');
}, true);body, html {
height: 100%;
}
.box-out {
width: 500px;
}
.box-out,
.box-middle,
.box-in {
padding: 30px;
}
.box-out {
background-color: yellowgreen;
}
.box-middle {
background-color: cadetblue;
}
.box-in {
background-color: pink;
}兩種機制的比較
| 項目 | 冒泡(Bubbling) | 捕獲(Capture) |
|---|---|---|
| 傳遞方向 | 由內而外 | 由外而內 |
| 第三個參數 | false(預設) | true |
| 常見用途 | 事件委派、一般互動 | 想在子元素之前先攔下事件 |
阻擋事件傳播 stopPropagation()
如果要阻擋事件向外擴張,就可以利用到 事件物件 中的方法:event.stopPropagation()。
<!DOCTYPE html>
<html>
<head>
<title>JavaScript - 事件傳遞</title>
</head>
<body>
<div class="box-out" id="BoxOut">
<div class="box-in" id="BoxIn"></div>
</div>
</body>
</html>document.querySelector('#BoxOut').addEventListener('click', () => {
console.log('out');
});
document.querySelector('#BoxIn').addEventListener('click', (e) => {
e.stopPropagation();
console.log('in');
});.box-out,
.box-in {
padding: 30px;
}
.box-out {
background-color: yellowgreen;
}
.box-in {
background-color: pink;
}小提醒
若同一個元素上綁了多個監聽器,stopPropagation() 只擋住 往其他節點傳播,同元素的其他監聽器照樣執行。要連同元素上剩下的監聽器一起攔掉,請改用 event.stopImmediatePropagation()。
取消預設行為 event.preventDefault()
在 HTML 的元素中,有某些元素都有預設行為。像是 <a> 會有連結的動作、<input type="submit"> 會有送出表單的動作。如果我們需要綁定某些事件在元素上,但元素上卻有一些預設的 HTML 行為,將會影響我們安排好的工作。這樣一來,取消預設行為 就是一件很重要的事情。
<a id="Link" href="http://tw.yahoo.com">前往 Yahoo!</a>document.querySelector('#Link').addEventListener('click', (e) => {
e.preventDefault();
});兩個方法別搞混
| 方法 | 阻止的是 | 典型場景 |
|---|---|---|
stopPropagation() | 事件繼續傳給其他節點 | 點彈窗內部時不要關掉彈窗 |
preventDefault() | 元素自己的預設行為 | 表單改用 JavaScript 送出、連結不跳頁 |
注意
兩者互不影響:呼叫 preventDefault() 事件仍會繼續冒泡;呼叫 stopPropagation() 連結也還是會跳頁。需要兩種效果就兩個都寫。
常見問題
冒泡與捕獲有什麼差別?
冒泡是從觸發事件的元素往外傳到根節點,捕獲則相反,由 window 一路往內傳到目標元素。addEventListener() 第三個參數為 true 時走捕獲,預設 false 走冒泡。
stopPropagation 與 preventDefault 差在哪裡?
stopPropagation() 阻止事件繼續往其他節點傳播,但不影響該元素本身的預設行為;preventDefault() 取消預設行為,例如:連結跳頁與表單送出,但事件仍會繼續傳播。
為什麼點子元素時父層的事件也被觸發了?
因為冒泡。事件從子元素往外傳,路徑上每個綁了同類事件的節點都會被觸發。不想讓它繼續往外,就在子元素的處理器裡呼叫 stopPropagation()。
所有事件都會冒泡嗎?
不是。focus、blur、mouseenter、mouseleave 這幾個不冒泡;click、input、change、keydown 則會。需要委派時請改用會冒泡的 對應事件。
stopPropagation 與 stopImmediatePropagation 有什麼不同?
stopPropagation() 只阻止事件傳到其他節點,同一元素上其他監聽器照樣執行;stopImmediatePropagation() 連同一元素剩下的監聽器都一起攔掉。