🌟 JS 事件监听全解析:从原理到实战(附代码逐行拆解)

143 阅读5分钟

一、事件是怎么“发生”的?—— 三步走的“事件接力赛”

1.1 DOM树的“快递投递”逻辑

想象你点击了页面上的一个按钮,这个动作会像快递包裹一样经历三段旅程:

<div id="parent">
  <div id="child"></div>
</div>

事件流三阶段

  1. 捕获阶段(快递从省→市→区)
    事件从document出发,经过<html><body>#parent#child
  2. 目标阶段(快递员敲门)
    事件到达#child,触发#child的监听函数
  3. 冒泡阶段(声音从房间→客厅→小区)
    事件从#child#parent<body>window
// 捕获阶段监听
document.getElementById('parent').addEventListener('click', () => {
  console.log('parent capture');
}, true)

// 冒泡阶段监听
document.getElementById('child').addEventListener('click', () => {
  console.log('child bubble');
}, false)
  • addEventListener的三个参数是:事件类型(如'click')、事件处理函数和一个布尔值(true表示捕获阶段,false表示冒泡阶段,默认false)。 输出顺序
    点击child时控制台输出:
parent capture
child bubble

二、事件机制详解:注册与触发的“暗号游戏”

2.1 事件监听的三种方式

事件监听就像给元素设置“暗号”,当特定动作发生时触发回调函数。

方式示例特点
DOM0级element.onclick = fn会被覆盖,不推荐
DOM2级addEventListener支持多监听器
事件委托父元素监听子元素事件性能优化神器
// DOM0级(不推荐)
document.getElementById('child').onclick = () => {
  console.log('DOM0级事件');
}

// DOM2级(推荐)
document.getElementById('child').addEventListener('click', () => {
  console.log('DOM2级事件');
})

2.2 事件异步执行的“时间差”

事件回调函数不会立即执行,而是放入任务队列等待:

console.log('同步代码'); // 立即执行
setTimeout(() => {
  console.log('宏任务'); // 事件队列等待
}, 0)
Promise.resolve().then(() => {
  console.log('微任务'); // 优先于宏任务
})

输出顺序

同步代码
微任务
宏任务

三、addEventListener 的“三个参数”奥秘

3.1 核心语法

addEventListener是现代浏览器推荐的事件绑定方式:

element.addEventListener(
  'click',                // 事件类型
  (event) => {            // 回调函数
    console.log(event.target);
  },
  false                   // useCapture(默认false)
)

3.2 useCapture 参数实战

通过useCapture参数控制事件监听的阶段:

// 捕获阶段监听
document.getElementById('parent').addEventListener('click', () => {
  console.log('捕获阶段');
}, true)

// 冒泡阶段监听
document.getElementById('child').addEventListener('click', () => {
  console.log('冒泡阶段');
}, false)

点击顺序

捕获阶段 → 冒泡阶段

四、事件委托:性能优化的“终极武器”

4.1 传统方式 vs 事件委托

为每个子元素绑定监听器 vs 在父元素统一监听:

<ul id="list">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

❌ 传统方式(内存爆炸)

const lis = document.querySelectorAll('#list li');
for (let i = 0; i < lis.length; i++) {
  lis[i].addEventListener('click', (e) => {
    console.log(e.target.innerHTML);
  })
}

✅ 事件委托(推荐)

document.getElementById('list').addEventListener('click', (e) => {
  console.log(e.target.innerHTML); // 动态元素自动生效
});
  • event.target指向触发事件的原始DOM元素,无论事件是否通过冒泡传播,它始终指向最初触发事件的那个元素。

总结

传统方式逐个为DOM元素添加事件监听器(如element.addEventListener)会导致内存爆炸,因为即使元素被移除,监听器仍会因引用关系滞留内存,形成内存泄漏。当元素数量庞大时,每个监听器的内存开销会累积,显著增加页面负担。相比之下,事件委托通过在父元素统一监听事件(利用事件冒泡机制),仅需一个监听器即可管理所有子元素的事件响应,不仅减少内存占用,还能自动适配动态添加/移除的元素,是高效且可维护的解决方案。

4.2 动态元素的“自适应”特性

新增的元素无需重新绑定监听器:

// 新增的<li>无需重新绑定
document.getElementById('list').addEventListener('click', (e) => {
  if (e.target.tagName === 'LI') {
    console.log('动态元素被点击:', e.target.innerHTML);
  }
});

五、事件对象 event 的“隐藏技能”

5.1 常用属性

事件对象event包含了事件的详细信息:

document.getElementById('child').addEventListener('click', (e) => {

  console.log('event.target:', e.target);    // 被点击的具体元素
  
  console.log('event.currentTarget:', e.currentTarget); // 绑定监听的元素
  
  e.stopPropagation(); // 阻止事件传播
  
  e.preventDefault();  // 阻止默认行为(如链接跳转)
});

5.2 阻止冒泡的“实战案例”

通过stopPropagation防止事件冒泡到父元素:

<div id="parent" onclick="alert('父元素')">
  <div id="child"></div>
</div>
<script>
  document.getElementById('child').addEventListener('click', (e) => {
    e.stopPropagation(); // 阻止冒泡到父元素
    console.log('子元素被点击');
  });
</script>
  • stopPropagation() 是事件对象的方法,用于阻止事件继续向上传播(冒泡)或向下传播(捕获),防止事件触发父元素或祖先元素的监听器。

六、常见问题与解决方案

6.1 事件冒泡导致的“误触发”

点击按钮时同时触发父元素事件:

<button onclick="handleClick()">点击我</button>
<script>
  function handleClick() {
    console.log('按钮点击');
  }
  document.body.addEventListener('click', () => {
    console.log('body点击');
  });
</script>

现象:点击按钮时输出两行日志
解决方案:在按钮事件中调用e.stopPropagation()

6.2 事件委托的“类型判断”

通过classList判断具体元素类型:

document.getElementById('list').addEventListener('click', (e) => {
  if (e.target.classList.contains('special')) {
    console.log('特殊元素被点击');
  }
});

七、进阶技巧:事件循环与性能优化

7.1 事件循环的“优先级规则”

理解微任务和宏任务的执行顺序:

console.log('Start');
setTimeout(() => {
  console.log('Timeout'); // 宏任务
}, 0);
Promise.resolve().then(() => {
  console.log('Promise'); // 微任务
});
console.log('End');

输出顺序

StartEnd → Promise → Timeout

7.2 事件节流与防抖

控制高频事件的触发频率:

// 防抖(搜索框输入)
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// 节流(窗口调整)
function throttle(fn, delay) {
  let flag = true;
  return (...args) => {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn(...args);
      flag = true;
    }, delay);
  };
}

八、总结:事件机制的“黄金法则”

  1. 事件流顺序:捕获 → 目标 → 冒泡

  2. 推荐监听方式addEventListener + 事件委托

  3. 性能优化技巧

    • 使用事件委托减少监听器数量
    • 合理使用stopPropagation防止冒泡

附机制图一张:

096244b625f640d5a6437c653400bcf3.png

🚀 实战建议
下次遇到动态列表时,优先使用事件委托;
在调试事件时,打印event.targetevent.currentTarget辅助定位;
用Chrome开发者工具的“Event Listeners”面板查看元素绑定的事件。