JavaScript系列06-深入理解 JavaScript 事件系统:从原生事件到 React 合成事件

news2025/3/10 14:26:02

JavaScript 事件系统是构建交互式 Web 应用的核心。本文从原生 DOM 事件到 React 的合成事件,内容涵盖:

  1. JavaScript 事件基础:事件类型、事件注册、事件对象
  2. 事件传播机制:捕获、目标和冒泡阶段
  3. 高级事件技术:事件委托、自定义事件
  4. 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 事件传播过程中有三个阶段:

  1. 捕获阶段:事件从 Window 对象向下传递到目标元素
  2. 目标阶段:事件到达目标元素
  3. 冒泡阶段:事件从目标元素向上冒泡到 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 事件委托的优势

  1. 减少事件监听器数量:一个监听器代替多个,减少内存消耗
  2. 动态元素处理:自动处理动态添加的元素
  3. 代码简洁:集中管理相关元素的事件处理

在这里插入图片描述

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 合成事件的特点

  1. 跨浏览器一致性:抹平不同浏览器的差异
  2. 性能优化:使用事件委托和事件池
  3. 自动绑定:React 组件中的事件处理方法可以自动绑定到组件实例

在这里插入图片描述

7.2 React 事件与原生事件的区别

  1. 命名约定:React 使用驼峰命名(如 onClick 而非 onclick
  2. 传递函数:React 传递函数作为事件处理程序,而不是字符串
  3. 返回 false 不阻止默认行为:必须显式调用 preventDefault()
  4. 事件委托:React 将大多数事件委托到根节点(document),而不是实际的 DOM 元素
  5. 合成事件对象:React 的事件对象是合成的,不是原生的

7.3 在 React 中使用事件

class ClickCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    
    // 绑定 this
    this.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();
    
    // 绑定 this
    this.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 的事件系统进行了重大更新:

  1. 事件委托位置变更:从 document 移动到了 React 树的根 DOM 容器,这使得在同一页面上运行多个 React 版本成为可能
  2. 去除事件池:合成事件对象不再被复用,不需要调用 e.persist()
  3. 对齐原生浏览器行为:如 onScroll 不再冒泡,onFocusonBlur 使用原生 focusin/focusout 事件

React 18 及更高版本继续保持这些更改,并进一步优化了事件系统的性能。

总结

掌握 JavaScript 事件系统不仅能帮助我们构建更好的用户界面,还能提高应用的性能和可维护性。无论是使用原生 JavaScript 还是现代前端框架,深入理解事件系统都是前端开发的必备技能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2312736.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大话机器学习三大门派:监督、无监督与强化学习

以武侠江湖为隐喻&#xff0c;系统阐述了机器学习的三大范式&#xff1a;​监督学习&#xff08;少林派&#xff09;​凭借标注数据精准建模&#xff0c;擅长图像分类等预测任务&#xff1b;无监督学习&#xff08;逍遥派&#xff09;​通过数据自组织发现隐藏规律&#xff0c;…

win11编译llama_cpp_python cuda128 RTX30/40/50版本

Geforce 50xx系显卡最低支持cuda128&#xff0c;llama_cpp_python官方源只有cpu版本&#xff0c;没有cuda版本&#xff0c;所以自己基于0.3.5版本源码编译一个RTX 30xx/40xx/50xx版本。 1. 前置条件 1. 访问https://developer.download.nvidia.cn/compute/cuda/12.8.0/local_…

FY-3D MWRI亮温绘制

1、FY-3D MWRI介绍 风云三号气象卫星&#xff08;FY-3&#xff09;是我国自行研制的第二代极轨气象卫星&#xff0c;其有效载荷覆 盖了紫外、可见光、红外、微波等频段&#xff0c;其目标是实现全球全天候、多光谱、三维定量 探测&#xff0c;为中期数值天气预报提供卫星观测数…

Codeforces1929F Sasha and the Wedding Binary Search Tree

目录 tags中文题面输入格式输出格式样例输入样例输出说明 思路代码 tags 组合数 二叉搜索树 中文题面 定义一棵二叉搜索树满足&#xff0c;点有点权&#xff0c;左儿子的点权 ≤ \leq ≤ 根节点的点权&#xff0c;右儿子的点权 ≥ \geq ≥ 根节点的点权。 现在给定一棵 …

HBuilder X 使用 TortoiseSVN 设置快捷键方法

HBuilder X 使用 TortoiseSVN 设置快捷键方法 单文件&#xff1a;(上锁&#xff0c;解锁&#xff0c;提交&#xff0c;更新) 安装好 TortoiseSVN &#xff0c;或者 按图操作&#xff1a; 1&#xff0c;工具栏中 【自定义快捷键】 2&#xff0c;点击 默认的快捷键设置&…

Java jar包后台运行方式详解

目录 一、打包成 jar 文件二、后台运行 jar 文件三、示例四、总结在 Java 开发中,我们经常需要将应用程序打包成可执行的 jar 文件,并在后台运行。这种方式对于部署长时间运行的任务或需要持续监听事件的应用程序非常重要。本文将详细介绍如何实现 Java jar 包的后台运行,并…

Mysql5.7-yum安装和更改mysql数据存放路径-2020年记录

记录下官网里用yum rpm源安装mysql, 1 官网下载rpm https://dev.mysql.com/downloads/repo/yum/ https://dev.mysql.com/doc/refman/5.7/en/linux-installation-yum-repo.html&#xff08;附官网操作手册&#xff09; wget https://repo.mysql.com//mysql80-community-release…

[项目]基于FreeRTOS的STM32四轴飞行器: 七.遥控器按键

基于FreeRTOS的STM32四轴飞行器: 七.遥控器 一.遥控器按键摇杆功能说明二.摇杆和按键的配置三.按键扫描 一.遥控器按键摇杆功能说明 两个手柄四个ADC。 左侧手柄&#xff1a; 前后推为飞控油门&#xff0c;左右推为控制飞机偏航角。 右侧手柄&#xff1a; 控制飞机飞行方向&a…

Android15使用FFmpeg解码并播放MP4视频完整示例

效果: 1.编译FFmpeg库: 下载FFmpeg-kit的源码并编译生成安装平台库 2.复制生成的FFmpeg库so文件与包含目录到自己的Android下 如果没有prebuiltLibs目录,创建一个,然后复制 包含目录只复制arm64-v8a下

安装树莓派3B+环境(嵌入式开发)

一、环境配置 1、下载树莓派镜像工具 点击进入下载连接 进入网站&#xff0c;点击下载即可。 2、配置wifi及ssh 将SD卡插入读卡器&#xff0c;再接入电脑&#xff0c;随后打开Raspberry Pi Imager下载工具&#xff0c; 选择Raspberry Pi 3 选择64位的操作系统 选择SD卡 选择…

p5.js:sound(音乐)可视化,动画显示音频高低变化

本文通过4个案例介绍了使用 p5.js 进行音乐可视化的实践&#xff0c;包括将音频振幅转化为图形、生成波形图。 承上一篇&#xff1a;vite&#xff1a;初学 p5.js demo 画圆圈 cd p5-demo copy .\node_modules\p5\lib\p5.min.js . copy .\node_modules\p5\lib\addons\p5.soun…

Linux下安装elasticsearch(Elasticsearch 7.17.23)

Elasticsearch 是一个分布式的搜索和分析引擎&#xff0c;能够以近乎实时的速度存储、搜索和分析大量数据。它被广泛应用于日志分析、全文搜索、应用程序监控等场景。 本文将带你一步步在 Linux 系统上安装 Elasticsearch 7.17.23 版本&#xff0c;并完成基本的配置&#xff0…

【The Rap of China】2018

中国新说唱第一季&#xff0c;2018 2018年4月13日&#xff0c;该节目通过官方微博宣布&#xff0c;其第二季将更名为《中国新说唱》。 《中国新说唱2018》由张震岳、MC Hotdog、潘玮柏、邓紫棋、WYF 担任明星制作人&#xff1b; 艾热获得冠军、那吾克热玉素甫江获得亚军、ICE…

通义万相2.1开源版本地化部署攻略,生成视频再填利器

2025 年 2 月 25 日晚上 11&#xff1a;00 通义万相 2.1 开源发布&#xff0c;前两周太忙没空搞它&#xff0c;这个周末&#xff0c;也来本地化部署一个&#xff0c;体验生成效果如何&#xff0c;总的来说&#xff0c;它在国内文生视频、图生视频的行列处于领先位置&#xff0c…

好玩的谷歌浏览器插件-自定义谷歌浏览器光标皮肤插件-Chrome 的自定义光标

周末没有啥事 看到了一个非常有意思的插件 就是 在使用谷歌浏览器的时候&#xff0c;可以把鼠标的默认样式换一个皮肤。就像下面的这种样子。 实际谷歌浏览器插件开发对于有前端编程基础的小伙伴 还是比较容易的&#xff0c;实际也是写 html css js 。 所以这个插件使用的技术…

svn删除所有隐藏.svn文件,文件夹脱离svn控制

新建一个文件&#xff0c;取名remove-svn-folders.reg&#xff0c;输入如下内容&#xff1a; Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\DeleteSVN] "Delete SVN Folders" [HKEY_LOCAL_MACHINE\SOFTWARE\Class…

六十天前端强化训练之第十二天之闭包深度解析

欢迎来到编程星辰海的博客讲解 目录 第一章&#xff1a;闭包的底层运行机制 1.1 词法环境&#xff08;Lexical Environment&#xff09;的构成JavaScript 引擎通过三个关键组件管理作用域&#xff1a; 1.2 作用域链的创建过程当函数被定义时&#xff1a; 1.3 闭包变量的生命…

DeepSeek R1-32B医疗大模型的完整微调实战分析(全码版)

DeepSeek R1-32B微调实战指南 ├── 1. 环境准备 │ ├── 1.1 硬件配置 │ │ ├─ 全参数微调:4*A100 80GB │ │ └─ LoRA微调:单卡24GB │ ├── 1.2 软件依赖 │ │ ├─ PyTorch 2.1.2+CUDA │ │ └─ Unsloth/ColossalAI │ └── 1.3 模…

10.2 继承与多态

文章目录 继承多态 继承 继承的作用是代码复用。派生类自动获得基类的除私有成员外的一切。基类描述一般特性&#xff0c;派生类提供更丰富的属性和行为。在构造派生类时&#xff0c;其基类构造函数先被调用&#xff0c;然后是派生类构造函数。在析构时顺序刚好相反。 // 基类…

[网络爬虫] 动态网页抓取 — Selenium 元素定位

&#x1f31f;想系统化学习爬虫技术&#xff1f;看看这个&#xff1a;[数据抓取] Python 网络爬虫 - 学习手册-CSDN博客 在使用 Selenium 时&#xff0c;往往需要先定位到指定元素&#xff0c;然后再执行相应的操作。例如&#xff0c;再向文本输入框中输入文字之前&#xff0c;…