4、事件对象封装:事件源、事件目标、事件数据、阻止默认行为与停止传播

事件对象,说白了就是浏览器在触发事件时,悄悄塞给你的那个「包裹」。里面装着谁触发了事件、事件发生在哪个元素上、带了什么数据,以及你能不能干预事件的后续行为。

我刚做前端那会儿,总觉得事件对象就是个参数,用的时候 console.log 看一眼就完事了。直到有一次做拖拽组件,发现 e.target 和 e.currentTarget 搞混了,拖拽逻辑全乱套。嗯,从那以后我才认真把事件对象的每个属性捋了一遍。

4.1 事件源与事件目标

这两个概念最容易混淆。我建议你先记住一句话:事件源是绑定监听器的元素,事件目标是实际触发事件的元素。

属性 含义 冒泡过程中是否变化
e.currentTarget 绑定事件监听器的元素(事件源) 不变
e.target 实际触发事件的元素(事件目标) 不变

核心区别:如果你在父元素上监听点击事件,点击子元素时,e.currentTarget 是父元素,e.target 是子元素。这就是事件委托能工作的底层原理。

// 事件委托示例
document.querySelector('.list').addEventListener('click', function(e) {
  // e.currentTarget 始终是 .list
  // e.target 是实际被点击的 li
  if (e.target.tagName === 'LI') {
    console.log('点击了列表项:', e.target.textContent);
  }
});

我在项目中遇到过一个问题:用事件委托做表格行点击,结果用户点到了行里的按钮,e.target 变成了按钮而不是行。解决方案很简单——用 e.currentTarget 或者 e.target.closest('tr') 向上查找。

4.2 事件数据

不同类型的事件会携带不同的数据。比如鼠标事件有 clientX/clientY,键盘事件有 keyCode,触摸事件有 touches 数组。

你想想看,如果每次都要自己从 DOM 元素上取数据,那得多麻烦。事件对象把这些数据都封装好了,直接拿来用就行。

事件类型 常用数据属性 说明
MouseEvent clientX, clientY, button 鼠标坐标、按键编号
KeyboardEvent key, code, ctrlKey 按键值、物理键位、修饰键
TouchEvent touches, changedTouches 触摸点列表
FocusEvent relatedTarget 焦点移入/移出的相关元素

小技巧:自定义事件可以通过 new CustomEvent('eventName', { detail: yourData }) 传递任意数据,然后在监听器里用 e.detail 取出来。这是组件间通信的轻量方案。

4.3 阻止默认行为

浏览器的很多元素都有默认行为。比如点击 a 标签会跳转,提交表单会刷新页面,右键会弹出菜单。有时候这些默认行为不是我们想要的。

e.preventDefault() 就是用来干这个的。调用之后,浏览器就不会执行默认动作了。

// 阻止链接跳转
document.querySelector('a.no-nav').addEventListener('click', function(e) {
  e.preventDefault();
  // 执行自定义逻辑,比如 SPA 路由切换
  navigateTo(this.getAttribute('href'));
});

// 阻止表单提交
document.querySelector('#myForm').addEventListener('submit', function(e) {
  e.preventDefault();
  // 用 AJAX 提交数据
  submitFormData(new FormData(this));
});

注意:preventDefault() 只阻止默认行为,不影响事件传播。也就是说,事件该冒泡还是冒泡,该捕获还是捕获。我曾经以为调了 preventDefault 事件就停了,结果父元素的监听器还是触发了,排查了半天。

4.4 停止传播

事件传播是 DOM 事件的核心机制。但有时候你不想让事件继续往上冒泡,或者不想让捕获阶段继续往下走。这时候就需要 e.stopPropagation()。

// 阻止冒泡
child.addEventListener('click', function(e) {
  e.stopPropagation();
  // 只有子元素的监听器会触发,父元素的不会
  console.log('只处理子元素点击');
});

parent.addEventListener('click', function() {
  // 如果子元素点击时调用了 stopPropagation,这里不会执行
  console.log('父元素点击');
});

还有一个 e.stopImmediatePropagation(),它更狠——不仅阻止传播,还阻止当前元素上其他同类型监听器的执行。

区别总结:

  • stopPropagation():阻止事件继续传播,但当前元素上绑定的其他同类型监听器仍然会执行
  • stopImmediatePropagation():阻止事件传播,并且当前元素上后续绑定的同类型监听器不再执行
element.addEventListener('click', function(e) {
  console.log('第一个监听器');
  e.stopImmediatePropagation(); // 第二个监听器不会执行
});

element.addEventListener('click', function() {
  console.log('第二个监听器'); // 这行不会输出
});

4.5 完整的事件对象封装结构

下面这张图展示了事件对象的核心结构,我建议你把它记在脑子里。

事件对象封装结构 Event 对象 事件源 (currentTarget) 事件目标 (target) 事件数据 (data) 控制方法 preventDefault() stopPropagation() stopImmediatePropagation() 💡 关键理解 • 事件源 (currentTarget) 和事件目标 (target) 在冒泡过程中可能不同 • 事件数据因事件类型而异,通过 e 对象直接访问 • 阻止默认行为不影响事件传播,停止传播不影响默认行为

4.6 实际项目中的封装实践

在实际项目中,我习惯把事件对象的常用操作封装成工具函数。这样既统一了行为,也方便维护。

// 事件工具封装
const EventUtils = {
  // 获取事件目标,支持选择器匹配
  getTarget(e, selector) {
    if (selector) {
      return e.target.closest(selector);
    }
    return e.target;
  },

  // 安全阻止默认行为
  preventDefault(e) {
    if (e.preventDefault) {
      e.preventDefault();
    } else {
      e.returnValue = false; // IE 兼容
    }
  },

  // 安全停止传播
  stopPropagation(e) {
    if (e.stopPropagation) {
      e.stopPropagation();
    } else {
      e.cancelBubble = true; // IE 兼容
    }
  },

  // 获取鼠标坐标(兼容各种浏览器)
  getMousePosition(e) {
    return {
      x: e.clientX || e.pageX,
      y: e.clientY || e.pageY
    };
  }
};

避坑指南:我曾经在移动端项目中,因为没处理 touch 事件的 preventDefault,导致页面滚动和手势操作冲突。后来统一在事件入口处判断是否需要阻止默认行为,才彻底解决。

4.7 阻止默认行为 vs 停止传播:什么时候用哪个?

这个问题我经常被问到。其实规则很简单:

  • 想阻止浏览器做某件事(比如跳转、刷新、滚动)→ 用 preventDefault()
  • 想阻止父元素收到事件通知(比如避免冒泡触发父级监听器)→ 用 stopPropagation()
  • 两者都想阻止 → 两个都调用

重要提醒:不要滥用 stopPropagation()。如果你在某个元素上阻止了冒泡,页面上其他依赖事件委托的功能可能会失效。我见过一个项目,因为到处调用 stopPropagation,导致全局点击统计完全失灵。

事件对象的封装,说白了就是让你能精准控制「谁触发了事件」「事件带了什么」「事件之后会发生什么」。把这几个概念理清楚,你在处理复杂交互时就会游刃有余。