JavaScript系列06-深入理解 JavaScript 事件系统:从原生事件到 React 合成事件
JavaScript 事件系统是构建交互式 Web 应用的核心。本文从原生 DOM 事件到 React 的合成事件,内容涵盖:
- JavaScript 事件基础:事件类型、事件注册、事件对象
- 事件传播机制:捕获、目标和冒泡阶段
- 高级事件技术:事件委托、自定义事件
- React 合成事件系统:特点、与原生事件的区别、使用方式
1. JavaScript 事件系统基础
JavaScript 事件系统是前端开发的核心机制之一,它允许网页对用户交互做出响应。简单来说,事件是在浏览器中发生的特定动作,如点击按钮、提交表单、加载页面等。
1.1事件的本质
从本质上讲,JavaScript 事件是一种观察者模式(Observer Pattern)的实现。在这种模式中:
- DOM 元素作为被观察者(Subject)
- 事件处理函数作为观察者(Observer)
- 当特定动作发生时,浏览器通知所有注册的观察者

1.2 基础事件类型
JavaScript 提供了众多内置事件类型,包括但不限于:
- 鼠标事件:
click,dblclick,mousedown,mouseup,mousemove,mouseover,mouseout - 键盘事件:
keydown,keypress,keyup - 表单事件:
submit,change,focus,blur - 窗口事件:
load,resize,scroll,unload - 触摸事件:
touchstart,touchmove,touchend,touchcancel
1.3 注册事件处理程序的方式
在 JavaScript 中,有三种主要的方式来注册事件处理程序:
- HTML 属性(不推荐)
<button onclick="handleClick()">点击我</button>
- DOM 属性
const button = document.querySelector('button');
button.onclick = function() {console.log('按钮被点击了');
};
- 事件监听器(推荐)
const button = document.querySelector('button');
button.addEventListener('click', function() {console.log('按钮被点击了');
});
使用 addEventListener 的优势在于:
- 可以为同一事件注册多个处理程序
- 提供更精细的控制(如捕获阶段)
- 可以轻松移除事件监听器
- 符合 W3C 标准
2. DOM 事件模型详解
DOM(文档对象模型)事件模型定义了事件如何在 DOM 树中传播,以及如何对其进行处理。
2.1 事件对象
当事件被触发时,浏览器会创建一个事件对象(Event object),并将其作为参数传递给事件处理函数。这个对象包含了与事件相关的各种信息:
document.addEventListener('click', function(event) {console.log('事件类型:', event.type);console.log('目标元素:', event.target);console.log('当前元素:', event.currentTarget);console.log('事件发生时间:', event.timeStamp);console.log('鼠标位置:', event.clientX, event.clientY);
});
常用的事件对象属性和方法包括:
| 属性/方法 | 描述 |
|---|---|
type | 事件类型(如 “click”, “load” 等) |
target | 触发事件的最深层 DOM 元素 |
currentTarget | 当前处理事件的 DOM 元素 |
preventDefault() | 阻止默认行为 |
stopPropagation() | 阻止事件冒泡 |
stopImmediatePropagation() | 阻止事件冒泡并阻止当前元素上的其他监听器被调用 |
timeStamp | 事件创建时的时间戳 |
3. 事件传播机制:捕获与冒泡
DOM 事件传播过程中有三个阶段:
- 捕获阶段:事件从 Window 对象向下传递到目标元素
- 目标阶段:事件到达目标元素
- 冒泡阶段:事件从目标元素向上冒泡到 Window 对象

3.1 捕获阶段
在捕获阶段,事件从 Window 开始,依次向下传递到目标元素。默认情况下,大多数事件处理程序都不会在捕获阶段被触发,除非在 addEventListener 方法的第三个参数中指定 true:
document.querySelector('div').addEventListener('click', function(event) {console.log('在捕获阶段处理点击事件');
}, true); // 第三个参数设为 true,表示在捕获阶段处理
3.2 冒泡阶段
在冒泡阶段,事件从目标元素开始,向上传递到 Window。默认情况下,事件处理程序在冒泡阶段被触发:
document.querySelector('button').addEventListener('click', function(event) {console.log('按钮被点击了');
}); // 默认在冒泡阶段处理document.querySelector('div').addEventListener('click', function(event) {console.log('div 的点击事件也被触发了(通过冒泡)');
}); // 默认在冒泡阶段处理
3.3 阻止事件传播
有时候我们需要阻止事件继续传播,可以使用 stopPropagation() 方法:
document.querySelector('button').addEventListener('click', function(event) {console.log('按钮被点击了');event.stopPropagation(); // 阻止事件冒泡// 上层元素的事件处理程序不会被调用
});
或者使用更强大的 stopImmediatePropagation(),它不仅阻止冒泡,还阻止当前元素上的其他监听器被调用:
document.querySelector('button').addEventListener('click', function(event) {console.log('这个处理程序会执行');event.stopImmediatePropagation();
});document.querySelector('button').addEventListener('click', function(event) {console.log('这个处理程序不会执行');
});
4. 事件处理模式与最佳实践
4.1 分离关注点
将事件监听与业务逻辑分离,使代码更易于维护:
// 不推荐
document.querySelector('button').addEventListener('click', function() {// 直接在这里处理复杂的业务逻辑const data = fetchData();processData(data);updateUI();
});// 推荐
function handleButtonClick() {const data = fetchData();processData(data);updateUI();
}document.querySelector('button').addEventListener('click', handleButtonClick);
4.2 命名事件处理函数
使用描述性的函数名,使代码更具可读性:
// 不推荐
button.addEventListener('click', function(e) { /* ... */ });// 推荐
button.addEventListener('click', handleSubmitForm);
button.addEventListener('click', validateUserInput);
4.3 移除不需要的事件监听器
当不再需要事件监听器时,应该及时移除它们,以防止内存泄漏:
function handleClick() {console.log('处理点击事件');
}// 添加事件监听器
const button = document.querySelector('button');
button.addEventListener('click', handleClick);// 当不再需要时移除事件监听器
button.removeEventListener('click', handleClick);
注意:要成功移除事件监听器,添加和移除时使用的必须是同一个函数引用,匿名函数无法被删除。
5. 事件委托:提升性能的关键技术
事件委托(Event Delegation)是一种利用事件冒泡机制的技术,它允许我们将事件监听器附加到父元素上,而不是直接附加到多个子元素上。
5.1 事件委托的优势
- 减少事件监听器数量:一个监听器代替多个,减少内存消耗
- 动态元素处理:自动处理动态添加的元素
- 代码简洁:集中管理相关元素的事件处理

5.2 实现事件委托
// 没有使用事件委托
// 为每个列表项添加事件监听器
document.querySelectorAll('li').forEach(item => {item.addEventListener('click', function() {console.log('列表项被点击:', this.textContent);});
});// 使用事件委托
// 只在父元素上添加一个事件监听器
document.querySelector('ul').addEventListener('click', function(event) {// 检查目标元素是否为列表项if (event.target.tagName === 'LI') {console.log('列表项被点击:', event.target.textContent);}
});
5.3 使用 closest() 方法优化事件委托
当处理嵌套元素时,event.target 可能是目标元素内部的子元素。这时可以使用 closest() 方法来查找最近的匹配元素:
document.querySelector('ul').addEventListener('click', function(event) {// 查找最近的 li 元素const listItem = event.target.closest('li');// 确保找到的元素在当前列表内if (listItem && this.contains(listItem)) {console.log('列表项被点击:', listItem.textContent);}
});
6. 自定义事件:扩展事件系统
除了浏览器提供的原生事件外,JavaScript 还允许我们创建和触发自定义事件,这对于组件间通信非常有用。
6.1 创建自定义事件
创建自定义事件有两种方式:
- 使用 Event 构造函数:
const event = new Event('build');// 监听事件
document.addEventListener('build', function(e) {console.log('构建事件被触发');
});// 触发事件
document.dispatchEvent(event);
- 使用 CustomEvent 构造函数(可以传递自定义数据):
const event = new CustomEvent('userLogin', {detail: {username: 'John',loginTime: new Date()}
});// 监听事件
document.addEventListener('userLogin', function(e) {console.log('用户登录:', e.detail.username);console.log('登录时间:', e.detail.loginTime);
});// 触发事件
document.dispatchEvent(event);
6.2自定义事件在组件通信中的应用
自定义事件可以用于在不直接相关的组件之间进行通信:
// 购物车组件
class ShoppingCart {constructor() {this.items = [];}addItem(item) {this.items.push(item);// 创建并触发自定义事件const event = new CustomEvent('cartUpdated', {detail: {itemCount: this.items.length,lastItemAdded: item}});document.dispatchEvent(event);}
}// 通知组件
class NotificationCenter {constructor() {// 监听购物车更新事件document.addEventListener('cartUpdated', this.handleCartUpdate.bind(this));}handleCartUpdate(event) {const { itemCount, lastItemAdded } = event.detail;this.showNotification(`添加了 ${lastItemAdded.name} 到购物车,当前共有 ${itemCount} 件商品`);}showNotification(message) {console.log('通知:', message);// 显示通知 UI}
}// 使用示例
const cart = new ShoppingCart();
const notifications = new NotificationCenter();cart.addItem({ id: 1, name: '手机', price: 999 });
7. React 合成事件系统
React 实现了自己的事件系统,称为"合成事件"(Synthetic Events)。它是对原生浏览器事件的跨浏览器包装,旨在使事件在不同浏览器中的行为一致。
7.1 合成事件的特点
- 跨浏览器一致性:抹平不同浏览器的差异
- 性能优化:使用事件委托和事件池
- 自动绑定:React 组件中的事件处理方法可以自动绑定到组件实例

7.2 React 事件与原生事件的区别
- 命名约定:React 使用驼峰命名(如
onClick而非onclick) - 传递函数:React 传递函数作为事件处理程序,而不是字符串
- 返回 false 不阻止默认行为:必须显式调用
preventDefault() - 事件委托:React 将大多数事件委托到根节点(document),而不是实际的 DOM 元素
- 合成事件对象:React 的事件对象是合成的,不是原生的
7.3 在 React 中使用事件
class ClickCounter extends React.Component {constructor(props) {super(props);this.state = { count: 0 };// 绑定 thisthis.handleClick = this.handleClick.bind(this);}handleClick(event) {// event 是 React 的合成事件对象console.log('事件类型:', event.type);console.log('目标元素:', event.target);// 更新状态this.setState(prevState => ({count: prevState.count + 1}));// 阻止默认行为event.preventDefault();}render() {return (<button onClick={this.handleClick}>点击了 {this.state.count} 次</button>);}
}
在函数组件中:
function ClickCounter() {const [count, setCount] = useState(0);const handleClick = (event) => {setCount(count + 1);};return (<button onClick={handleClick}>点击了 {count} 次</button>);
}
7.4 事件处理函数中的 this 绑定
在 React 类组件中,事件处理函数的 this 默认不指向组件实例,有以下几种解决方法:
- 在构造函数中绑定:
constructor(props) {super(props);this.handleClick = this.handleClick.bind(this);
}
- 使用箭头函数:
// 在类中使用箭头函数定义方法
handleClick = (event) => {this.setState({ count: this.state.count + 1 });
};
- 在渲染时使用箭头函数(不推荐,每次渲染会创建新函数):
render() {return <button onClick={(e) => this.handleClick(e)}>点击</button>;
}
7.5 合成事件与原生事件的交互
在某些情况下,需要同时使用 React 合成事件和 DOM 原生事件:
class HybridEventComponent extends React.Component {constructor(props) {super(props);this.buttonRef = React.createRef();// 绑定 thisthis.handleResize = this.handleResize.bind(this);this.handleClick = this.handleClick.bind(this);this.handleNativeClick = this.handleNativeClick.bind(this);}componentDidMount() {// 添加原生事件监听器window.addEventListener('resize', this.handleResize);// 添加到 DOM 元素this.buttonRef.current.addEventListener('click', this.handleNativeClick);}componentWillUnmount() {// 记得清理!window.removeEventListener('resize', this.handleResize);this.buttonRef.current.removeEventListener('click', this.handleNativeClick);}handleResize(event) {console.log('窗口大小改变 - 原生事件');// 这里的 event 是原生 DOM 事件对象}handleClick(event) {console.log('按钮点击 - React 合成事件');// 这里的 event 是 React 合成事件对象}handleNativeClick(event) {console.log('按钮点击 - 原生事件');// 这里的 event 是原生 DOM 事件对象}render() {return (<button ref={this.buttonRef} onClick={this.handleClick}>点击我</button>);}
}
7.6 React 17 中的事件系统更新
在 React 17 中,React 的事件系统进行了重大更新:
- 事件委托位置变更:从
document移动到了 React 树的根 DOM 容器,这使得在同一页面上运行多个 React 版本成为可能 - 去除事件池:合成事件对象不再被复用,不需要调用
e.persist() - 对齐原生浏览器行为:如
onScroll不再冒泡,onFocus和onBlur使用原生 focusin/focusout 事件
React 18 及更高版本继续保持这些更改,并进一步优化了事件系统的性能。
总结
掌握 JavaScript 事件系统不仅能帮助我们构建更好的用户界面,还能提高应用的性能和可维护性。无论是使用原生 JavaScript 还是现代前端框架,深入理解事件系统都是前端开发的必备技能。
相关文章:
JavaScript系列06-深入理解 JavaScript 事件系统:从原生事件到 React 合成事件
JavaScript 事件系统是构建交互式 Web 应用的核心。本文从原生 DOM 事件到 React 的合成事件,内容涵盖: JavaScript 事件基础:事件类型、事件注册、事件对象事件传播机制:捕获、目标和冒泡阶段高级事件技术:事件委托、…...
C++:string容器(下篇)
1.string浅拷贝的问题 // 为了和标准库区分,此处使用String class String { public :/*String():_str(new char[1]){*_str \0;}*///String(const char* str "\0") // 错误示范//String(const char* str nullptr) // 错误示范String(const char* str …...
2.数据结构-栈和队列
数据结构-栈和队列 2.1栈2.1.1栈的表示和实现2.1.2栈的应用举例数制转换括号匹配检验迷宫给求解表达式求值 2.1.3链栈的表示和实现2.1.4栈与递归的实现遍历输出链表中各个结点的递归算法*Hanoi塔问题的递归算法 2.2队列2.2.1循环队列——队列的顺序表示和实现2.2.2链队——队列…...
aws(学习笔记第三十一课) aws cdk深入学习(batch-arm64-instance-type)
aws(学习笔记第三十一课) aws cdk深入学习 学习内容: 深入练习aws cdk下部署batch-arm64-instance-type 1. 深入练习aws cdk下部署batch-arm64-instance-type 代码链接 代码链接 代码链接 -> batch-arm64-instance-type之前代码学习 之前学习代码链接 -> aw…...
MySQL 中,SELECT ... FOR UPDATE
在 MySQL 中,SELECT ... FOR UPDATE 语句会对查询结果集中的行加排他锁(X 锁)。关于其他事务是否能读取当前行,以下是详细说明: 1. 排他锁(X 锁)的特性 排他锁是一种独占锁,加锁后&…...
云服务运维智能时代:阿里云操作系统控制台
阿里云操作系统控制台 引言需求介绍操作系统使用实例获得的帮助与提升建议 引言 阿里云操作系统控制台是一款创新型云服务器运维工具,专为简化用户的运维工作而设计。它采用智能化和可视化的方式,让运维变得更加高效、直观。借助AI技术,控制…...
【Agent的革命之路——LangGraph】如何使用config
有时我们希望在调用代理时能够对其进行配置。这包括配置使用哪个语言模型(LLM)等例子。下面我们将通过一个示例来详细介绍如何进行这样的配置。 在介绍 configurable 之前我们先介绍一下 Langchain 的 RunnableConfig。RunnableConfig是一个配置对象&…...
ArcGIS操作:15 计算点的经纬度,并添加到属性表
注意:需要转化为地理坐标系 1、打开属性表,添加字段 2、计算字段(以计算纬度为例 !Shape!.centroid.Y ) 3、效果...
Docker基础入门
第 1 章:核心概念与安装配置 本章首先介绍Docker 的三大核心概念: 镜像 (Image)容器(Container)仓库(Repository) 只有理解了这三个核心概念,才能顺利地理解Docker容器的整个生命周期。 随后࿰…...
【Linux】详谈 基础I/O
目录 一、理解文件 狭义的理解: 广义理解: 文件操作的归类认知 系统角度 二、系统文件I/O 2.1 标志位的传递 系统级接口open 编辑 open返回值 写入文件 读文件 三、文件描述符 3.1(0 & 1 & 2) 3.2 文件描…...
爬虫案例七Python协程爬取视频
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、Python协程爬取视频 前言 提示:这里可以添加本文要记录的大概内容: 爬虫案例七协程爬取视频 提示:以下是本篇文章正文…...
[20250304] 关于 RISC-V芯片 的介绍
[20250304] 关于 RISC-V芯片 的介绍 1. 调研报告 一、RISC-V 芯片结构分析 RISC-V 芯片基于开源指令集架构(ISA),其核心优势在于模块化设计与高度灵活性。 指令集架构 基础指令集:包含 RV32I(32 位)、R…...
一学就会:A*算法详细介绍(Python)
📢本篇文章是博主人工智能学习以及算法研究时,用于个人学习、研究或者欣赏使用,并基于博主对相关等领域的一些理解而记录的学习摘录和笔记,若有不当和侵权之处,指出后将会立即改正,还望谅解。文章分类在&am…...
Hadoop、Hive、Spark的关系
Part1:Hadoop、Hive、Spark关系概览 1、MapReduce on Hadoop 和spark都是数据计算框架,一般认为spark的速度比MR快2-3倍。 2、mapreduce是数据计算的过程,map将一个任务分成多个小任务,reduce的部分将结果汇总之后返回。 3、HIv…...
Excel·VBA江西省预算一体化工资表一键处理
每月制作工资表导出为Excel后都需要调整格式,删除0数据的列、对工资表项目进行排序、打印设置等等,有些单位还分有“行政”、“事业”2个工资表就需要操作2次。显然,这种重复操作的问题,可以使用VBA代码解决 目录 代码使用说明1&a…...
23种设计模式简介
一、创建型(5种) 1.工厂方法 总店定义制作流程,分店各自实现特色披萨(北京店-烤鸭披萨,上海店-蟹粉披萨) 2.抽象工厂 套餐工厂(家庭装含大披萨薯条,情侣装含双拼披萨红酒&#…...
python fire 库与 sys.argv 处理命令行参数
fire库 Python Fire 由Google开发,它使得命令行接口(CLI)的创建变得容易。使用Python Fire,可以将Python对象(如类、函数或字典)转换为可以从终端运行的命令行工具。这能够以一种简单而直观的方式与你的Py…...
PDF处理控件Aspose.PDF,如何实现企业级PDF处理
PDF处理为何成为开发者的“隐形雷区”? “手动调整200页PDF目录耗时3天,扫描件文字识别错误导致数据混乱,跨平台渲染格式崩坏引发客户投诉……” 作为开发者,你是否也在为PDF处理的复杂细节消耗大量精力?Aspose.PDF凭…...
Spring(1)——mvc概念,部分常用注解
1、什么是Spring Web MVC? Spring MVC 是一种基于 Java 的实现了 MVC(Model-View-Controller,模型 - 视图 - 控制器)设计模式的 Web 应用框架,它是 Spring 框架的一个重要组成部分,用于构建 Web 应用程序。…...
C语言(23)
字符串函数 11.strstr函数 1.1函数介绍: 头文件:string.h char *strstr ( const char * str1,const char *str2); 作用:在一个字符串(str1)中寻找另外一个字符串(str2)是否出现过 如果找到…...
龙虎榜——20250610
上证指数放量收阴线,个股多数下跌,盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型,指数短线有调整的需求,大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的:御银股份、雄帝科技 驱动…...
网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...
return this;返回的是谁
一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请,不同级别的经理有不同的审批权限: // 抽象处理者:审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...
LLMs 系列实操科普(1)
写在前面: 本期内容我们继续 Andrej Karpathy 的《How I use LLMs》讲座内容,原视频时长 ~130 分钟,以实操演示主流的一些 LLMs 的使用,由于涉及到实操,实际上并不适合以文字整理,但还是决定尽量整理一份笔…...
AI语音助手的Python实现
引言 语音助手(如小爱同学、Siri)通过语音识别、自然语言处理(NLP)和语音合成技术,为用户提供直观、高效的交互体验。随着人工智能的普及,Python开发者可以利用开源库和AI模型,快速构建自定义语音助手。本文由浅入深,详细介绍如何使用Python开发AI语音助手,涵盖基础功…...
上位机开发过程中的设计模式体会(1):工厂方法模式、单例模式和生成器模式
简介 在我的 QT/C 开发工作中,合理运用设计模式极大地提高了代码的可维护性和可扩展性。本文将分享我在实际项目中应用的三种创造型模式:工厂方法模式、单例模式和生成器模式。 1. 工厂模式 (Factory Pattern) 应用场景 在我的 QT 项目中曾经有一个需…...
