魯斯前端布魯斯前端

文章中英模式

布魯斯前端面試題目 - 事件冒泡原理解析

深入理解JavaScript事件冒泡與捕獲機制,掌握事件委派的高效實現,以及如何處理複雜UI中的事件傳播問題。

影片縮圖

懶得看文章?那就來看影片吧

DOM事件流:冒泡與捕獲

當DOM元素上發生事件時,事件會按照特定順序傳播。根據W3C事件模型,事件傳播分為三個階段:捕獲階段、目標階段和冒泡階段。

document
html
body
div
button (target)
Click me
捕獲階段
document → html → body → div → button
冒泡階段
button → div → body → html → document

捕獲階段 (Capturing Phase)

事件從DOM樹的頂部(document)向下傳播到目標元素的過程。

標準事件監聽器中,設置第三個參數為true即可在捕獲階段捕獲事件:

element.addEventListener('click', handler, true);

冒泡階段 (Bubbling Phase)

事件從目標元素向上傳播到DOM樹頂部(document)的過程。

默認情況下,事件處理器在冒泡階段被調用:

element.addEventListener('click', handler);

element.addEventListener('click', handler, false);

// 模擬事件流
document.addEventListener('click', () => console.log('1. 捕獲: document'), true);
document.addEventListener('click', () => console.log('6. 冒泡: document'), false);

document.body.addEventListener('click', () => console.log('2. 捕獲: body'), true);
document.body.addEventListener('click', () => console.log('5. 冒泡: body'), false);

const button = document.querySelector('button');
button.addEventListener('click', () => console.log('3. 捕獲: button'), true);
button.addEventListener('click', () => console.log('4. 冒泡: button'), false);

// 點擊button時的輸出順序:
// 1. 捕獲: document
// 2. 捕獲: body
// 3. 捕獲: button
// 4. 冒泡: button
// 5. 冒泡: body
// 6. 冒泡: document

事件委派(Event Delegation)

事件委派是利用事件冒泡機制,將事件監聽器綁定到父元素上,通過判斷事件來源處理子元素事件的一種模式。這種方法有效減少事件監聽器數量,提高性能,尤其適用於動態添加的元素。

事件委派的優勢

  • 1. 減少事件監聽器數量,節約內存
  • 2. 簡化動態元素的事件處理
  • 3. 減少代碼量和維護成本
  • 4. 自動處理後期添加的元素
// 不使用事件委派 - 每個按鈕都有自己的事件監聽器
document.querySelectorAll('.button').forEach(button => {
  button.addEventListener('click', function() {
    console.log('按鈕被點擊:', this.textContent);
  });
});

// 使用事件委派 - 只在父元素上設置一個監聽器
document.querySelector('.button-container').addEventListener('click', function(e) {
  // 檢查是否點擊的是按鈕
  if (e.target.classList.contains('button')) {
    console.log('按鈕被點擊:', e.target.textContent);
  }
});

React中的事件委派

React實現了自己的事件系統,自動使用事件委派,將大多數事件處理器附加到document上,而不是實際的DOM元素。

// React自動實現事件委派
function TodoList() {
  const [todos, setTodos] = useState(['學習React', '理解事件冒泡', '實踐事件委派']);
  
  const handleItemClick = (index) => {
    console.log(`點擊了第${index + 1}項: ${todos[index]}`);
  };
  
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index} onClick={() => handleItemClick(index)}>
          {todo}
        </li>
      ))}
    </ul>
  );
}

// React內部只在document級別設置一個監聽器,無需為每個li添加

阻止事件傳播

有時需要阻止事件繼續冒泡或捕獲,防止觸發父元素的事件處理器,可以使用以下方法:

event.stopPropagation()

阻止事件沿DOM樹向上冒泡,但不會阻止當前元素上的其他事件處理器。

常見用例:

- 防止點擊模態框內部時關閉模態框

- 阻止嵌套菜單中的點擊事件影響父級菜單

event.stopImmediatePropagation()

不僅阻止事件冒泡,還會阻止當前元素上後續的事件處理器執行。

常見用例:

- 確保特定條件下完全取消事件處理

- 在多個插件或庫共存時控制事件處理優先級

// 阻止冒泡示例
document.querySelector('.child').addEventListener('click', function(e) {
  console.log('子元素被點擊');
  
  // 阻止事件冒泡到父元素
  e.stopPropagation();
});

document.querySelector('.parent').addEventListener('click', function() {
  console.log('父元素被點擊 - 冒泡階段'); // 不會執行
});

// stopImmediatePropagation示例
const button = document.querySelector('button');

button.addEventListener('click', function(e) {
  console.log('第一個處理器');
  e.stopImmediatePropagation(); // 阻止後續處理器和冒泡
});

button.addEventListener('click', function() {
  console.log('第二個處理器'); // 不會執行
});

阻止默認行為 vs 阻止冒泡

開發者經常混淆preventDefault()stopPropagation(),但它們有不同的用途:

event.preventDefault()

阻止元素的默認行為,但不影響事件傳播。

  • 1. 阻止表單提交的頁面刷新
  • 2. 阻止鏈接的跳轉行為
  • 3. 阻止複選框的選中/取消選中
  • 4. 阻止右鍵菜單顯示

event.stopPropagation()

阻止事件冒泡或捕獲,但不影響元素默認行為。

  • 1. 阻止事件傳播到父元素
  • 2. 防止父元素事件處理器被觸發
  • 3. 不會阻止表單提交、鏈接跳轉等
  • 4. 事件仍然會在當前元素完成處理
// 兩者的區別
document.querySelector('a').addEventListener('click', function(e) {
  // 阻止跳轉行為,但事件仍會冒泡到父元素
  e.preventDefault();
  
  // 進行其他操作,如AJAX請求
  console.log('鏈接被點擊,但不會跳轉');
});

document.querySelector('.clickable').addEventListener('click', function(e) {
  // 阻止冒泡,但如果這個元素是鏈接,仍然會跳轉
  e.stopPropagation();
  
  console.log('事件不會傳播到父元素');
});

// 同時使用兩者
document.querySelector('form button').addEventListener('click', function(e) {
  // 阻止表單提交
  e.preventDefault();
  
  // 阻止事件冒泡
  e.stopPropagation();
  
  console.log('表單不會提交,事件不會冒泡');
});

🔥 常見面試題目

1. 請說明事件冒泡、事件委派、捕獲是什麼?

概念說明
事件冒泡事件從觸發元素開始,向上傳播到父元素,一直到document和window
事件捕獲事件從window開始,向下傳播到目標元素,與冒泡方向相反
事件委派利用冒泡機制,將事件處理器設置在父元素上,通過event.target處理子元素事件
// 事件委派示例
document.querySelector('ul').addEventListener('click', function(e) {
  // 檢查是否點擊了li元素
  if (e.target.tagName === 'LI') {
    console.log('點擊了列表項:' + e.target.textContent);
  }
});

2. 如何避免冒泡?

有三種主要方法可以阻止事件冒泡:

  1. event.stopPropagation():阻止事件繼續冒泡,但不影響當前元素的其他事件處理器
  2. event.stopImmediatePropagation():阻止事件冒泡,並阻止當前元素的其他事件處理器執行
  3. 在捕獲階段處理事件:通過addEventListener的第三個參數設為true,在捕獲階段處理事件
// 阻止冒泡示例
element.addEventListener('click', function(e) {
  e.stopPropagation();
  console.log('事件不會冒泡到父元素');
});

// 捕獲階段處理事件
parent.addEventListener('click', function(e) {
  console.log('捕獲階段處理,先於目標元素');
}, true);

3. event.stopPropagation() 和 event.preventDefault() 有什麼區別?

方法作用常見用途
stopPropagation()阻止事件傳播(冒泡或捕獲)防止點擊子元素時觸發父元素事件
preventDefault()阻止元素的默認行為阻止表單提交、鏈接跳轉、複選框選中
// preventDefault() 示例
document.querySelector('a').addEventListener('click', function(e) {
  e.preventDefault(); // 阻止鏈接跳轉
  console.log('鏈接被點擊,但不會跳轉');
});

// 兩者結合使用
document.querySelector('form').addEventListener('submit', function(e) {
  e.preventDefault(); // 阻止表單提交
  e.stopPropagation(); // 阻止事件冒泡
  console.log('表單驗證失敗,不提交且不冒泡');
});