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

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

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

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

  • 用户操作(比如点击)就像发了一个快递请求。
  • JS 把这个请求放到一个队列里排队。
  • 当主线程空了,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 复制代码
<!-- 方式一: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() 方法。
  • 支持多个监听器绑定到同一个事件上。
  • 可以指定是在 捕获阶段 还是 冒泡阶段 触发。
  • 更加灵活、强大。

示例:

js 复制代码
const btn = document.getElementById("btn");

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

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

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


捕获 vs 冒泡:

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

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

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

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

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

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

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

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


2. 阻止事件传播

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

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

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

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

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

📋 总结一句话:

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


🔁 对比总结表

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

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

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


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


三、🧩 addEventListener() 是什么?

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

基本写法:

js 复制代码
element.addEventListener(type, listener, options);

或者:

js 复制代码
element.addEventListener(type, listener, useCapture);

1. 参数详解

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

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

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

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

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

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

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

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

button.addEventListener("click", handleClick);

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


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

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


2. useCapture 参数(布尔值)

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

  • true:在捕获阶段触发(从外向内)
  • false:在冒泡阶段触发(从内向外)------ 默认值
示例:
html 复制代码
<div id="outer">
  <div id="inner">点我</div>
</div>
js 复制代码
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 是一个对象,可以包含以下选项:

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

✅ 1) once: 只触发一次

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

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


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

适合移动端滚动优化:

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

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


✅ 3) signal: 动态取消监听

配合 AbortController 使用:

js 复制代码
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.signal AbortSignal 手动控制取消监听 { signal }

四、🧠 什么是事件委托?

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

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


🏠 类比理解

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

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

✅ 在网页中:

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

✅ 举个例子

HTML 结构:

html 复制代码
<ul id="menu">
  <li>首页</li>
  <li>关于我们</li>
  <li>联系我们</li>
</ul>

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

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

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

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

js 复制代码
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 当前正在处理事件的元素(也就是绑定监听器的那个父元素)
js 复制代码
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>

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

相关推荐
袁煦丞7 分钟前
数据库设计神器DrawDB:cpolar内网穿透实验室第595个成功挑战
前端·程序员·远程工作
天天扭码15 分钟前
从图片到语音:我是如何用两大模型API打造沉浸式英语学习工具的
前端·人工智能·github
Liudef0616 分钟前
2048小游戏实现
javascript·css·css3
独行soc1 小时前
#渗透测试#批量漏洞挖掘#HSC Mailinspector 任意文件读取漏洞(CVE-2024-34470)
linux·科技·安全·网络安全·面试·渗透测试
鱼樱前端2 小时前
今天介绍下最新更新的Vite7
前端·vue.js
coder_pig2 小时前
跟🤡杰哥一起学Flutter (三十四、玩转Flutter手势✋)
前端·flutter·harmonyos
万少2 小时前
01-自然壁纸实战教程-免费开放啦
前端
独立开阀者_FwtCoder2 小时前
【Augment】 Augment技巧之 Rewrite Prompt(重写提示) 有神奇的魔法
前端·javascript·github
yuki_uix3 小时前
AI辅助网页设计:从图片到代码的实践探索
前端
我想说一句3 小时前
事件机制与委托:从冒泡捕获到高效编程的奇妙之旅
前端·javascript