FunTester JavaScript 事件系统入门:DOM 事件与事件流

FunTester · 2025年11月04日 · 48 次阅读

前言

事件处理贯穿前端开发的方方面面:从按钮点击、表单输入,到列表滚动、模块通信,良好的事件设计直接影响到代码的可维护性与用户体验。本文聚焦 DOM 事件基础与事件流机制,帮助你在实际项目中写出既稳又优的交互逻辑。

读者定位与阅读收获

  • 面向:入门到中级前端工程师,追求工程化与可维护性
  • 收获:弄清捕获与冒泡差异、正确移除监听器、规避常见陷阱

DOM 事件基础

什么是事件

事件是用户或浏览器触发的特定动作,例如:点击 click、鼠标移动 mousemove、页面加载 load、输入变化 input 等。通过监听事件并编写处理函数,页面就能对这些动作做出响应。

监听事件的三种方式

方式一:HTML 内联(不推荐)

<button onclick="handleClick()">点击我</button>
<script>
function handleClick() {
  alert('按钮被点击了!');
}
</script>

缺点:HTML 与 JavaScript 强耦合,不利于维护与复用。

方式二:DOM 属性

<button id="myButton">点击我</button>
<script>
const button = document.getElementById('myButton');
button.onclick = function() {
  alert('按钮被点击了!');
};
</script>

缺点:同一事件只能绑定一个处理函数。

方式三:addEventListener(推荐)

<button id="myButton">点击我</button>
<script>
const button = document.getElementById('myButton');

// 可以绑定多个处理函数
button.addEventListener('click', function() {
  console.log('第一个处理函数');
});

button.addEventListener('click', function() {
  console.log('第二个处理函数');
});
</script>

优点:

  • 可绑定多个处理函数
  • 可控制触发阶段(捕获或冒泡)
  • 可精确移除监听器

移除事件监听

function handleClick() {
  console.log('点击了!');
}

const button = document.getElementById('myButton');
button.addEventListener('click', handleClick);

// 移除监听(必须使用同一函数引用)
button.removeEventListener('click', handleClick);

常见错误:使用匿名函数导致无法移除。

// ❌ 错误:无法移除匿名函数
button.addEventListener('click', function() {
  console.log('点击了!');
});
button.removeEventListener('click', function() {
  console.log('点击了!');
});

// ✅ 正确:使用命名函数或保存函数引用
const handler = function() {
  console.log('点击了!');
};
button.addEventListener('click', handler);
button.removeEventListener('click', handler);

事件冒泡与捕获

事件流的三个阶段

当一个事件发生时,它会经历三个阶段:

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

基础 API:

  • addEventListener(type, listener, useCapture):第三个参数为 true 表示在捕获阶段触发,默认 false 表示在冒泡阶段触发
  • removeEventListener(type, listener, useCapture):移除监听时参数需与添加时完全一致 提示:移除监听器时,类型、回调函数引用与第三个参数必须与添加时严格一致,否则无法移除。

实例演示

<div id="outer" style="padding: 50px; background: lightblue;">
  外层 DIV
  <div id="inner" style="padding: 30px; background: lightcoral;">
    内层 DIV
    <button id="button">点击我</button>
  </div>
</div>

<script>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
const button = document.getElementById('button');

// 冒泡阶段(默认)
button.addEventListener('click', () => {
  console.log('按钮 - 冒泡');
});
inner.addEventListener('click', () => {
  console.log('内层 DIV - 冒泡');
});
outer.addEventListener('click', () => {
  console.log('外层 DIV - 冒泡');
});

// 捕获阶段(第三个参数设为 true)
outer.addEventListener('click', () => {
  console.log('外层 DIV - 捕获');
}, true);
inner.addEventListener('click', () => {
  console.log('内层 DIV - 捕获');
}, true);
button.addEventListener('click', () => {
  console.log('按钮 - 捕获');
}, true);

// 点击按钮时,典型输出顺序:
// 外层 DIV - 捕获
// 内层 DIV - 捕获
// 按钮 - 捕获
// 按钮 - 冒泡
// 内层 DIV - 冒泡
// 外层 DIV - 冒泡
</script>

阻止传播与默认行为

// 阻止冒泡传播
button.addEventListener('click', (e) => {
  console.log('按钮被点击');
  e.stopPropagation();
});

// 阻止同一元素其他监听器
button.addEventListener('click', (e) => {
  console.log('第一个监听器');
  e.stopImmediatePropagation();
});
button.addEventListener('click', () => {
  console.log('第二个监听器 - 不会执行');
});

// 阻止默认行为(如链接跳转)
const link = document.querySelector('a');
link.addEventListener('click', (e) => {
  e.preventDefault();
  console.log('链接被点击,但不会跳转');
});

适用场景与建议

  • 复杂嵌套交互:合理使用捕获/冒泡,避免在深层节点重复绑定监听
  • 列表或表格点击:优先使用事件委托,降低绑定与内存开销
  • 组件封装:在组件根节点统一处理事件,对外仅暴露清晰回调

易错点清单

  1. 使用匿名函数绑定导致无法 removeEventListener
  2. 循环中使用 var 绑定事件引发索引错误
  3. 误用 stopImmediatePropagation 影响同元素其他监听器
  4. 忘记销毁时清理监听,造成内存泄漏

FunTester 原创精华
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暫無回覆。
需要 登录 後方可回應,如果你還沒有帳號按這裡 注册