你以为 React 的事件很简单?错了,它暗藏玄机!

27 阅读9分钟

🧠 一、什么是 JavaScript 的事件机制?

JavaScript 是一种 单线程语言,也就是说它一次只能做一件事。但网页上有很多事情是用户随时会做的(比如点击按钮、输入文字等),JavaScript 就需要一种方式来处理这些“突发事件”,这就是 事件机制

你可以把事件机制想象成一个“快递员”系统:

  • 用户操作(比如点击)就像发了一个快递请求。
  • JS 把这个请求放到一个队列里排队。
  • 当主线程空了,JS 再一个个取出来处理。

🔁 异步执行

举个例子:

console.log("开始");

document.getElementById("btn").addEventListener("click", function() {
  console.log("你点了按钮");
});

console.log("结束");

输出顺序是这样的:

开始
结束
(当你点按钮时才会输出)你点了按钮

解释:

  • console.log("开始")console.log("结束") 是同步代码,立刻执行。
  • 点击事件是异步的 —— 只有当用户真的去点击按钮时才会执行。

✅ 所以说:事件是异步的,不会马上执行,而是等用户操作后才触发。


二、JavaScript 中的两种事件模型:DOM0 级 & DOM2 级事件


🌟 为什么需要了解这些?

JavaScript 的事件处理方式经历了几个阶段,不同的写法适用于不同年代的浏览器。现在虽然主要用 DOM2 级事件,但理解这些有助于你更好地理解 React 和现代前端框架是怎么处理事件的。


1️⃣ DOM0 级事件 —— 最早的方式

✅ 特点:

  • 直接在 HTML 标签中或 JS 中给元素添加 on事件名 属性。
  • 只能绑定一个事件处理函数。
  • 如果重复绑定,后面的会覆盖前面的。

示例:

<!-- 方式一:HTML 内联绑定 -->
<button onclick="alert('Hello')">点我</button>

<!-- 方式二:JS 中绑定 -->
<script>
  const btn = document.getElementById("btn");
  btn.onclick = function() {
    alert("你点了按钮!");
  };
</script>

缺点:

  • 不够灵活,不能多次绑定同一个事件。
  • 代码和结构混在一起,不便于维护。

2️⃣ DOM1 级事件? —— 并不存在!

❓那 DOM1 是什么?

  • DOM1(Document Object Model Level 1)是 W3C 发布的一个标准,主要是定义了文档的基本结构和操作方法。
  • 并没有涉及事件处理机制
  • 所以说:DOM1 级事件这个说法其实是不存在的

3️⃣ DOM2 级事件 —— 推荐使用的方式

✅ 特点:

  • 使用 addEventListener() 方法。
  • 支持多个监听器绑定到同一个事件上。
  • 可以指定是在 捕获阶段 还是 冒泡阶段 触发。
  • 更加灵活、强大。

示例:

const btn = document.getElementById("btn");

btn.addEventListener("click", function() {
  alert("第一次点击");
});

btn.addEventListener("click", function() {
  alert("第二次点击");
});

两个提示都会弹出,不会互相覆盖!


捕获 vs 冒泡:

你可以把事件想象成一颗石头掉进水里:

  • 捕获阶段(Capture Phase):石头往水里沉下去(从最外层向目标元素传播)。
  • 冒泡阶段(Bubble Phase):水花溅上来(从目标元素向外传播)。

React 的事件命名也体现了这一点:

  • onClick → 冒泡阶段触发
  • onClickCapture → 捕获阶段触发

🧩 实际应用场景
1. 事件委托

使用冒泡阶段可以非常方便地实现事件委托。例如,你可以在一个列表项很多的情况下,只给整个列表添加一个事件监听器,而不是为每一个列表项都添加监听器。

document.getElementById('list').addEventListener('click', function(event) {
    if (event.target.tagName === 'LI') {
        console.log('你点击了一个列表项');
    }
});

这样不仅减少了内存占用,还简化了代码逻辑。


2. 阻止事件传播

有时候你需要阻止某个事件继续传播,比如在一个模态框(Modal)内部点击时,不希望触发背后的页面点击事件。你可以通过在捕获阶段阻止事件传播来达到目的。

document.getElementById('modal').addEventListener('click', function(event) {
    event.stopPropagation(); // 阻止事件继续冒泡
}, true); // 使用捕获阶段

🤔 为什么不只用一个阶段?

如果只保留一个阶段(无论是捕获还是冒泡),都会失去一些重要的灵活性:

  • 仅捕获阶段:无法轻松实现常见的用户交互,因为大多数情况下我们希望事件能够“冒泡”上来。
  • 仅冒泡阶段:则失去了在事件到达目标之前进行处理的机会,这对于某些高级功能(如全局拦截、权限控制)是必要的。

📋 总结一句话:

捕获阶段和冒泡阶段各有其独特的用途和优势,它们共同提供了强大的灵活性,使得我们可以根据不同的需求选择合适的时机来处理事件。


🔁 对比总结表

特性DOM0 级事件DOM2 级事件
绑定方式onclick 属性或赋值addEventListener()
是否支持多个监听器否(会被覆盖)
是否支持捕获/冒泡控制
是否推荐使用❌ 不推荐✅ 推荐
是否容易维护❌ 差(逻辑与结构混合)✅ 好

🧠 小贴士:React 用了哪种方式?

React 的合成事件系统底层其实用的是 DOM2 级事件addEventListener),只是封装了一层,让你不用手动管理事件绑定和解绑,更加安全高效。


✅ 现代网页开发都使用 addEventListener() 来监听事件,它更灵活、功能更强,是主流做法。


三、🧩 addEventListener() 是什么?

它是 JavaScript 中用来监听事件的标准方法,比如点击、输入、滚动等。

基本写法:

element.addEventListener(type, listener, options);

或者:

element.addEventListener(type, listener, useCapture);

1. 参数详解

1) type:要监听的事件类型(字符串)

就是你想监听哪种操作,比如:

  • "click" 点击
  • "input" 输入框内容变化
  • "scroll" 滚动页面
  • "keydown" 键盘按键按下

✅ 注意:这个值是大小写敏感的,一般都用小写。

示例:
document.getElementById("btn").addEventListener("click", function() {
  alert("按钮被点击了");
});

2) listener:事件发生时执行的函数

它是一个函数或实现了 EventListener 接口的对象。

✅ 最常见的是一个函数:
function handleClick(event) {
  console.log("你点我了");
}

button.addEventListener("click", handleClick);

⚠️ 注意:不要加括号 (),因为我们要传的是函数本身,不是调用结果。


3) optionsuseCapture:可选参数,控制监听行为

这是高级功能,我们可以根据需求选择是否使用。


2. useCapture 参数(布尔值)

决定是在 捕获阶段 还是 冒泡阶段 触发事件。

  • true:在捕获阶段触发(从外向内)
  • false:在冒泡阶段触发(从内向外)—— 默认值
示例:
<div id="outer">
  <div id="inner">点我</div>
</div>
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");

// 捕获阶段监听
outer.addEventListener("click", () => {
  console.log("外层:捕获阶段");
}, true);

// 冒泡阶段监听
outer.addEventListener("click", () => {
  console.log("外层:冒泡阶段");
}, false);

inner.addEventListener("click", () => {
  console.log("内层被点击");
});
输出顺序(点击“内层”):
外层:捕获阶段
内层被点击
外层:冒泡阶段

3. options 参数对象(React 和现代项目常用)

options 是一个对象,可以包含以下选项:

属性类型描述
captureBooleanuseCapture,是否在捕获阶段触发
onceBoolean只触发一次,之后自动移除监听器
passiveBoolean表示不会调用 preventDefault(),用于优化性能(如滚动)
signalAbortSignal配合 AbortController 使用,可以手动取消监听

✅ 1) once: 只触发一次

button.addEventListener("click", () => {
  console.log("只执行一次!");
}, { once: true });

点击第一次会输出,第二次就不会了。


✅ 2) passive: 不阻止默认行为,提高滚动性能

适合移动端滚动优化:

window.addEventListener("wheel", () => {
  console.log("滚动了");
}, { passive: true });

如果用了 { passive: true },就不能再调用 event.preventDefault(),否则浏览器会警告。


✅ 3) signal: 动态取消监听

配合 AbortController 使用:

const controller = new AbortController();
const signal = controller.signal;

button.addEventListener("click", () => {
  console.log("点击了一次");
}, { signal });

controller.abort(); // 主动取消监听

4. 总结对比表

参数名类型作用示例
type字符串事件类型,如 "click""scroll"
listener函数或对象事件触发时执行的函数handleClick
useCapture布尔值是否在捕获阶段触发true / false
options.capture布尔值同上{ capture: true }
options.once布尔值只触发一次后自动移除{ once: true }
options.passive布尔值不调用 preventDefault(),提升性能{ passive: true }
options.signalAbortSignal手动控制取消监听{ signal }

四、🧠 什么是事件委托?

事件委托,就是把子元素的事件监听任务交给父元素来做。

听起来有点抽象?没关系,我们用小白也能懂的方式解释。


🏠 类比理解

想象你是一个小区的保安:

  • 小区里有 100 户人家。
  • 如果你给每家每户门口都装一个摄像头、安排一个人看守,成本太高了。
  • 所以你选择在小区大门装一个摄像头,谁进来了你都知道 —— 这就是“事件委托”。

✅ 在网页中:

  • 子元素很多的时候,每个都加监听器效率低。
  • 把监听器放在它们的共同父元素上,通过冒泡机制统一处理。

✅ 举个例子

HTML 结构:

<ul id="menu">
  <li>首页</li>
  <li>关于我们</li>
  <li>联系我们</li>
</ul>

你想点击每个 <li> 的时候弹出对应的菜单名。

不推荐的做法(每个都加监听器):

document.querySelectorAll("#menu li").forEach(item => {
  item.addEventListener("click", function() {
    alert(this.textContent);
  });
});

推荐做法(使用事件委托):

document.getElementById("menu").addEventListener("click", function(event) {
  if (event.target.tagName === "LI") {
    alert(event.target.textContent);
  }
});

🔍 原理讲解

因为事件会冒泡,所以:

  • 点击的是 <li>,但事件会一直冒泡到它的父元素 <ul>
  • 我们在 <ul> 上监听点击事件,然后判断 event.target 是哪个元素触发的
  • 如果是 <li>,就执行对应的操作

💡 event.target 和 event.currentTarget 的区别

属性含义
event.target实际被点击的元素(比如某个 <li>
event.currentTarget当前正在处理事件的元素(也就是绑定监听器的那个父元素)
document.getElementById("menu").addEventListener("click", function(event) {
  console.log("event.target:", event.target); // 被点击的 <li>
  console.log("event.currentTarget:", event.currentTarget); // 绑定事件的 <ul>
});

🚀 事件委托的优点(为什么大家都喜欢用它)

优点说明
性能更好不需要为每个子元素单独绑定事件,减少内存消耗
动态添加元素也有效新增的 <li> 也会自动被委托处理,不需要重新绑定
代码更简洁只需写一个监听函数,就可以处理多个子元素

📌 使用场景举例

  • 表格、列表项很多时(如聊天消息、商品列表)
  • 动态加载内容(比如 Ajax 加载新数据)
  • 导航栏、选项卡等交互组件
  • React 中的事件系统其实也是基于事件委托实现的!

❗ 注意事项

  • 只能用于支持事件冒泡的事件类型(如 click、input),不适用于 focus、blur 等
  • 要注意判断 event.target,避免误操作其他嵌套元素(比如 <li> 里面有 <span>

事件委托是一种利用事件冒泡机制,将多个子元素的事件监听集中到父元素上的优化技术,既节省资源又便于维护。