Skip to content

JavaScript 事件傳播:冒泡與捕獲機制完整說明

事件傳播:冒泡與捕獲

事件傳播 = 事件流,就是描述頁面中接受事件的順序。大致上,可以分成兩種機制:

  • 冒泡(Bubbling)。
  • 捕獲(Capture)。

小提醒

一次點擊實際上會跑完三個階段:捕獲階段(由外而內)→ 目標階段(到達被點的元素)→ 冒泡階段(由內而外)。我們平常寫的監聽器只是決定要掛在哪個階段上。

冒泡

代表從觸發事件的元素開始,向外傳遞,直到網頁的根節點為止。如下方程式碼所示,我們點擊了 <h1> 元素,事件將會層層向外傳遞。

其順序為:<h1><body><html>documentwindow

html
<!DOCTYPE html>
<html>
  <head>
    <title>JavaScript - 事件傳遞</title>
  </head>
  <body>
    <h1>click</h1>
  </body>
</html>

測試以下程式碼,從最內層向外傳遞。以 console.log() 印出來的內容,

依序為:in → middle → out → body → html → document → window。

html
<!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>
js
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);
css
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 都是向外傳遞的冒泡機制。

捕獲

此傳遞的方式剛好與冒泡相反,也就是由外向內傳遞。以下方的程式碼為例:

其順序為:windowdocument<html><body><h1>

html
<!DOCTYPE html>
<html>
  <head>
    <title>JavaScript - 事件傳遞</title>
  </head>
  <body>
    <h1>click</h1>
  </body>
</html>

測試以下程式碼,從最外層向內傳遞。以 console.log() 印出來的內容,

依序為:window → document → html → body → out → middle → in。

html
<!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>
js
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);
css
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()

html
<!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>
js
document.querySelector('#BoxOut').addEventListener('click', () => {
  console.log('out');
});

document.querySelector('#BoxIn').addEventListener('click', (e) => {
  e.stopPropagation();
  console.log('in');
});
css
.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 行為,將會影響我們安排好的工作。這樣一來,取消預設行為 就是一件很重要的事情。

html
<a id="Link" href="http://tw.yahoo.com">前往 Yahoo!</a>
js
document.querySelector('#Link').addEventListener('click', (e) => {
  e.preventDefault();
});

兩個方法別搞混

方法阻止的是典型場景
stopPropagation()事件繼續傳給其他節點點彈窗內部時不要關掉彈窗
preventDefault()元素自己的預設行為表單改用 JavaScript 送出、連結不跳頁

注意

兩者互不影響:呼叫 preventDefault() 事件仍會繼續冒泡;呼叫 stopPropagation() 連結也還是會跳頁。需要兩種效果就兩個都寫。

常見問題

冒泡與捕獲有什麼差別?

冒泡是從觸發事件的元素往外傳到根節點,捕獲則相反,由 window 一路往內傳到目標元素。addEventListener() 第三個參數為 true 時走捕獲,預設 false 走冒泡。

stopPropagation 與 preventDefault 差在哪裡?

stopPropagation() 阻止事件繼續往其他節點傳播,但不影響該元素本身的預設行為;preventDefault() 取消預設行為,例如:連結跳頁與表單送出,但事件仍會繼續傳播。

為什麼點子元素時父層的事件也被觸發了?

因為冒泡。事件從子元素往外傳,路徑上每個綁了同類事件的節點都會被觸發。不想讓它繼續往外,就在子元素的處理器裡呼叫 stopPropagation()

所有事件都會冒泡嗎?

不是。focusblurmouseentermouseleave 這幾個不冒泡;clickinputchangekeydown 則會。需要委派時請改用會冒泡的 對應事件

stopPropagation 與 stopImmediatePropagation 有什麼不同?

stopPropagation() 只阻止事件傳到其他節點,同一元素上其他監聽器照樣執行;stopImmediatePropagation() 連同一元素剩下的監聽器都一起攔掉。

延伸閱讀