DOM 事件机制

事件捕获、冒泡、事件委托及 target 与 currentTarget 的区别

问题

解释 DOM 事件机制,包括捕获、冒泡、事件委托,以及 targetcurrentTarget 的区别。

解答

事件流的三个阶段

DOM 事件传播分为三个阶段:

  1. 捕获阶段:从 window 向下传播到目标元素
  2. 目标阶段:到达目标元素
  3. 冒泡阶段:从目标元素向上传播到 window
window

  ▼ 捕获阶段
document


html


body


div (目标元素) ← 目标阶段

  ▼ 冒泡阶段
body


...回到 window

捕获与冒泡

// addEventListener 第三个参数控制监听阶段
// true: 捕获阶段触发
// false(默认): 冒泡阶段触发

const outer = document.getElementById('outer');
const inner = document.getElementById('inner');

// 冒泡阶段触发(默认)
outer.addEventListener('click', () => {
  console.log('outer 冒泡');
});

// 捕获阶段触发
outer.addEventListener('click', () => {
  console.log('outer 捕获');
}, true);

inner.addEventListener('click', () => {
  console.log('inner 冒泡');
});

inner.addEventListener('click', () => {
  console.log('inner 捕获');
}, true);

// 点击 inner 时输出顺序:
// outer 捕获 -> inner 捕获 -> inner 冒泡 -> outer 冒泡

target 与 currentTarget

const parent = document.getElementById('parent');
const child = document.getElementById('child');

parent.addEventListener('click', (e) => {
  // target: 实际被点击的元素(事件源)
  console.log('target:', e.target);
  
  // currentTarget: 绑定事件处理器的元素(始终是 parent)
  console.log('currentTarget:', e.currentTarget);
});

// 点击 child 时:
// target: <div id="child">
// currentTarget: <div id="parent">

// 点击 parent 时:
// target: <div id="parent">
// currentTarget: <div id="parent">

事件委托

利用冒泡机制,在父元素上统一处理子元素的事件:

<ul id="list">
  <li data-id="1">Item 1</li>
  <li data-id="2">Item 2</li>
  <li data-id="3">Item 3</li>
</ul>
const list = document.getElementById('list');

// 不推荐:给每个 li 绑定事件
// document.querySelectorAll('li').forEach(li => {
//   li.addEventListener('click', handler);
// });

// 推荐:事件委托
list.addEventListener('click', (e) => {
  // 判断点击的是否是 li 元素
  if (e.target.tagName === 'LI') {
    const id = e.target.dataset.id;
    console.log('点击了:', id);
  }
});

// 更健壮的写法:处理点击 li 内部元素的情况
list.addEventListener('click', (e) => {
  const li = e.target.closest('li');
  if (li && list.contains(li)) {
    console.log('点击了:', li.dataset.id);
  }
});

阻止传播与默认行为

element.addEventListener('click', (e) => {
  // 阻止事件继续传播(捕获或冒泡)
  e.stopPropagation();
  
  // 阻止同一元素上的其他监听器执行
  e.stopImmediatePropagation();
  
  // 阻止默认行为(如链接跳转、表单提交)
  e.preventDefault();
});

完整示例

<!DOCTYPE html>
<html>
<body>
  <div id="grandparent" style="padding: 50px; background: #f0f0f0;">
    Grandparent
    <div id="parent" style="padding: 50px; background: #ddd;">
      Parent
      <div id="child" style="padding: 50px; background: #bbb;">
        Child
      </div>
    </div>
  </div>

  <script>
    const elements = ['grandparent', 'parent', 'child'];
    
    elements.forEach(id => {
      const el = document.getElementById(id);
      
      // 捕获阶段
      el.addEventListener('click', (e) => {
        console.log(`${id} 捕获, target: ${e.target.id}`);
      }, true);
      
      // 冒泡阶段
      el.addEventListener('click', (e) => {
        console.log(`${id} 冒泡, target: ${e.target.id}`);
      });
    });
    
    // 点击 child 输出:
    // grandparent 捕获, target: child
    // parent 捕获, target: child
    // child 捕获, target: child
    // child 冒泡, target: child
    // parent 冒泡, target: child
    // grandparent 冒泡, target: child
  </script>
</body>
</html>

关键点

  • 事件流:捕获阶段 → 目标阶段 → 冒泡阶段
  • addEventListener 第三个参数为 true 时在捕获阶段触发
  • target 是触发事件的元素,currentTarget 是绑定监听器的元素
  • 事件委托利用冒泡,在父元素上处理子元素事件,减少内存占用,支持动态元素
  • stopPropagation() 阻止传播,preventDefault() 阻止默认行为