通俗易懂的React事件系统工作原理

news2025/7/18 8:17:06

前言

React 为我们提供了一套虚拟的事件系统,这套虚拟事件系统是如何工作的,笔者对源码做了一次梳理,整理了下面的文档供大家参考。

在 React事件介绍 中介绍了合成事件对象以及为什么提供合成事件对象,主要原因是因为 React 想实现一个全浏览器的框架, 为了实现这种目标就需要提供全浏览器一致性的事件系统,以此抹平不同浏览器的差异。

合成事件对象很有意思,一开始听名字会觉得很奇怪,看到英文名更奇怪 SyntheticEvent, 实际上合成事件的意思就是使用原生事件合成一个 React 事件, 例如使用原生click事件合成了onClick事件,使用原生mouseout事件合成了onMouseLeave事件,原生事件和合成事件类型大部分都是一一对应,只有涉及到兼容性问题时我们才需要使用不对应的事件合成。合成事件并不是 React 的首创,在 iOS 上遇到的 300ms 问题而引入的 fastclick 就使用了 touch 事件合成了 click 事件,也算一种合成事件的应用。

了解了 React 事件是合成事件之后我们看待事件的角度就会有所不同, 例如我们经常在代码中写的这种代码

<button onClick={handleClick}>
  Activate Lasers
</button>

我们已经知道这个onClick只是一个合成事件而不是原生事件, 那这段时间究竟发生了什么? 原生事件和合成事件是如何对应起来的?

上面的代码看起来很简洁,实际上 React 事件系统工作机制比起上面要复杂的多,脏活累活全都在底层处理了, 简直框架劳模。其工作原理大体上分为两个阶段

  1. 事件绑定
  2. 事件触发

下面就一起来看下这两个阶段究竟是如何工作的, 这里主要从源码层分析,并以 16.13 源码中内容为基准。

1. React 是如何绑定事件的 ?

React 既然提供了合成事件,就需要知道合成事件与原生事件是如何对应起来的,这个对应关系存放在 React 事件插件中EventPlugin, 事件插件可以认为是 React 将不同的合成事件处理函数封装成了一个模块,每个模块只处理自己对应的合成事件,这样不同类型的事件种类就可以在代码上解耦,例如针对onChange事件有一个单独的LegacyChangeEventPlugin插件来处理,针对onMouseEnteronMouseLeave 使用 LegacyEnterLeaveEventPlugin 插件来处理。

为了知道合成事件与原生事件的对应关系,React 在一开始就将事件插件全部加载进来, 这部分逻辑在 ReactDOMClientInjection 代码如下

injectEventPluginsByName({
    SimpleEventPlugin: LegacySimpleEventPlugin,
    EnterLeaveEventPlugin: LegacyEnterLeaveEventPlugin,
    ChangeEventPlugin: LegacyChangeEventPlugin,
    SelectEventPlugin: LegacySelectEventPlugin,
    BeforeInputEventPlugin: LegacyBeforeInputEventPlugin
});

注册完上述插件后, EventPluginRegistry (老版本代码里这个模块唤作EventPluginHub)这个模块里就初始化好了一些全局对象,有几个对象比较重要,可以单独说一下。

第一个对象是 registrationNameModule, 它包含了 React 事件到它对应的 plugin 的映射, 大致长下面这样,它包含了 React 所支持的所有事件类型,这个对象最大的作用是判断一个组件的 prop 是否是事件类型,这在处理原生组件的 props 时候将会用到,如果一个 prop 在这个对象中才会被当做事件处理。

{
    onBlur: SimpleEventPlugin,
    onClick: SimpleEventPlugin,
    onClickCapture: SimpleEventPlugin,
    onChange: ChangeEventPlugin,
    onChangeCapture: ChangeEventPlugin,
    onMouseEnter: EnterLeaveEventPlugin,
    onMouseLeave: EnterLeaveEventPlugin,
    ...
}

第二个对象是 registrationNameDependencies, 这个对象长下面几个样子

{
    onBlur: ['blur'],
    onClick: ['click'],
    onClickCapture: ['click'],
    onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
    onMouseEnter: ['mouseout', 'mouseover'],
    onMouseLeave: ['mouseout', 'mouseover'],
    ...
}

这个对象即是一开始我们说到的合成事件到原生事件的映射,对于onClickonClickCapture事件, 只依赖原生click事件。但是对于 onMouseLeave它却是依赖了两个mouseoutmouseover, 这说明这个事件是 React 使用 mouseoutmouseover 模拟合成的。正是因为这种行为,使得 React 能够合成一些哪怕浏览器不支持的事件供我们代码里使用。参考React实战视频讲解:进入学习

第三个对象是 plugins, 这个对象就是上面注册的所有插件列表。

plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];

看完上面这些信息后我们再反过头来看下一个普通的EventPlugin长什么样子。一个 plugin 就是一个对象, 这个对象包含了下面两个属性

// event plugin
{
  eventTypes, // 一个数组,包含了所有合成事件相关的信息,包括其对应的原生事件关系
  extractEvents: // 一个函数,当原生事件触发时执行这个函数
}

了解上面这这些信息对我们分析 React 事件工作原理将会很有帮助,下面开始进入事件绑定阶段。

  1. React 执行 diff 操作,标记出哪些 DOM 类型 的节点需要添加或者更新。

  1. 当检测到需要创建一个节点或者更新一个节点时, 使用 registrationNameModule 查看一个 prop 是不是一个事件类型,如果是则执行下一步。

  1. 通过 registrationNameDependencies 检查这个 React 事件依赖了哪些原生事件类型

  1. 检查这些一个或多个原生事件类型有没有注册过,如果有则忽略。

  1. 如果这个原生事件类型没有注册过,则注册这个原生事件到 document 上,回调为React提供的dispatchEvent函数。

上面的阶段说明:

  1. 我们将所有事件类型都注册到 document 上。
  2. 所有原生事件的 listener 都是dispatchEvent函数。
  3. 同一个类型的事件 React 只会绑定一次原生事件,例如无论我们写了多少个onClick, 最终反应在 DOM 事件上只会有一个listener
  4. React 并没有将我们业务逻辑里的listener绑在原生事件上,也没有去维护一个类似eventlistenermap的东西存放我们的listener

由 3,4 条规则可以得出,我们业务逻辑的listener和实际 DOM 事件压根就没关系,React 只是会确保这个原生事件能够被它自己捕捉到,后续由 React 来派发我们的事件回调,当我们页面发生较大的切换时候,React 可以什么都不做,从而免去了去操作removeEventListener或者同步eventlistenermap的操作,所以其执行效率将会大大提高,相当于全局给我们做了一次事件委托,即便是渲染大列表,也不用开发者关心事件绑定问题。

2. React 是如何触发事件的?

我们知道由于所有类型种类的事件都是绑定为React的 dispatchEvent 函数,所以就能在全局处理一些通用行为,下面就是整个行为过程。

export function dispatchEventForLegacyPluginEventSystem(
  topLevelType: DOMTopLevelEventType,  eventSystemFlags: EventSystemFlags,  nativeEvent: AnyNativeEvent,  targetInst: null | Fiber,
): void {
  const bookKeeping = getTopLevelCallbackBookKeeping(
    topLevelType,
    nativeEvent,
    targetInst,
    eventSystemFlags
  );

  try {
    // Event queue being processed in the same cycle allows
    // `preventDefault`.
    batchedEventUpdates(handleTopLevel, bookKeeping);
  } finally {
    releaseTopLevelCallbackBookKeeping(bookKeeping);
  }
}

bookKeeping为事件执行时组件的层级关系存储,也就是如果在事件执行过程中发生组件结构变更,并不会影响事件的触发流程。

整个触发事件流程如下:

  1. 任意一个事件触发,执行 dispatchEvent 函数。
  2. dispatchEvent 执行 batchedEventUpdates(handleTopLevel), batchedEventUpdates 会打开批量渲染开关并调用 handleTopLevel
  3. handleTopLevel 会依次执行 plugins 里所有的事件插件。
  4. 如果一个插件检测到自己需要处理的事件类型时,则处理该事件。

对于大部分事件而言其处理逻辑如下,也即 LegacySimpleEventPlugin 插件做的工作

  1. 通过原生事件类型决定使用哪个合成事件类型(原生 event 的封装对象,例如 SyntheticMouseEvent) 。
  2. 如果对象池里有这个类型的实例,则取出这个实例,覆盖其属性,作为本次派发的事件对象(事件对象复用),若没有则新建一个实例。

  1. 从点击的原生事件中找到对应 DOM 节点,从 DOM 节点中找到一个最近的React组件实例, 从而找到了一条由这个实例父节点不断向上组成的链, 这个链就是我们要触发合成事件的链,(只包含原生类型组件, diva 这种原生组件)。

  1. 反向触发这条链,父-> 子,模拟捕获阶段,触发所有 props 中含有 onClickCapture 的实例。

  1. 正向触发这条链,子-> 父,模拟冒泡阶段,触发所有 props 中含有 onClick 的实例。

这几个阶段说明了下面的现象:

  1. React 的合成事件只能在事件周期内使用,因为这个对象很可能被其他阶段复用, 如果想持久化需要手动调用event.persist() 告诉 React 这个对象需要持久化。( React17 中被废弃)
  2. React 的冒泡和捕获并不是真正 DOM 级别的冒泡和捕获
  3. React 会在一个原生事件里触发所有相关节点的 onClick 事件, 在执行这些onClick之前 React 会打开批量渲染开关,这个开关会将所有的setState变成异步函数。
  4. 事件只针对原生组件生效,自定义组件不会触发 onClick

3. 从React 的事件系统中我们学到了什么

  1. React16 将原生事件都绑定在 document 上.

这点很好理解,React的事件实际上都是在document上触发的。

  1. 我们收到的 event 对象为 React 合成事件, event 对象在事件之外不可以使用

所以下面就是错误用法

function onClick(event) {
    setTimeout(() => {
        console.log(event.target.value);
    }100);
}
  1. React 会在派发事件时打开批量更新, 此时所有的 setState 都会变成异步。
function onClick(event) {
    setState({a: 1}); // 1
    setState({a: 2}); // 2
    setTimeout(() => {
        setState({a: 3}); // 3
        setState({a: 4}); // 4
    }0);
}

此时 1, 2 在事件内所以是异步的,二者只会触发一次 render 操作,3, 4 是同步的,3,4 分别都会触发一次 render。

  1. React onClick/onClickCapture, 实际上都发生在原生事件的冒泡阶段。
document.addEventListener('click', console.log.bind(null'native'));

function onClickCapture() {
    console.log('capture');
}

<div onClickCapture={onClickCapture}/>

这里我们虽然使用了onClickCapture, 但实际上对原生事件而言依然是冒泡,所以 React 16 中实际上就不支持绑定捕获事件。

  1. 由于所有事件都注册到顶层事件上,所以多实个 ReactDOM.render 会存在冲突。

如果我们渲染一个子树使用另一个版本的 React 实例创建, 那么即使在子树中调用了 e.stopPropagatio 事件依然会传播。所以多版本的 React 在事件上存在冲突。

最后我们就可以轻松理解 React 事件系统的架构图了

4. React 17 中事件系统有哪些新特性

React 17 目前已经发布了, 官方称之为没有新特性的更新, 对于使用者而言没有提供类似 Hooks 这样爆炸的特性,也没有 Fiber 这样的重大重构,而是积攒了大量 Bugfix,修复了之前存在的诸多缺陷。其中变化最大的就数对事件系统的改造了。

下面是笔者列举的一些事件相关的特性更新

调整将顶层事件绑在container上,ReactDOM.render(app, container);

react_17_delegation

将顶层事件绑定在 container 上而不是 document 上能够解决我们遇到的多版本共存问题,对微前端方案是个重大利好。

对齐原生浏览器事件

React 17 中终于支持了原生捕获事件的支持, 对齐了浏览器原生标准。

同时onScroll 事件不再进行事件冒泡。

onFocusonBlur 使用原生 focusinfocusout 合成。

Aligning with Browsers
We’ve made a couple of smaller changes related to the event system:
The onScroll event no longer bubbles to prevent common confusion.
React onFocus and onBlur events have switched to using the native focusin and focusout events under the hood, which more closely match React’s existing behavior and sometimes provide extra information.
Capture phase events (e.g. onClickCapture) now use real browser capture phase listeners.

取消事件复用

官方的解释是事件对象的复用在现代浏览器上性能已经提高的不明显了,反而还很容易让人用错,所以干脆就放弃这个优化。

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

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

相关文章

【附源码】Python计算机毕业设计图书商城购物系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;我…

MongoDB学习一:相关概念和单机部署

目录一、MongoDB 应用场景&#xff1a;二、什么时候使用MongoDB&#xff1a;三、MongoDB简介&#xff1a;四、体系结构&#xff1a;五、数据模型&#xff1a;六、MongoDB的特点&#xff1a;七、MongoDB单机部署一、MongoDB 应用场景&#xff1a; 二、什么时候使用MongoDB&#…

对FD描述符(包括inode以及三张表)的一点理解

文件描述符&#xff0c;简单来说是一个从0开始递增的非负整数。 具体来说是linux/unix对文件系统的一种底层抽象&#xff0c;这种抽象是通过三张表来实现的。 这三张表分别是&#xff1a; 1.进程级的文件描述符表&#xff1b;(文件标志位/文件指针) 2.系统级的打开文件描述…

Android Studio App开发之下载管理器DownloadManager中显示、轮询下载进度、利用POST上传文件讲解及实战(附源码)

运行有问题或需要源码请点赞关注收藏后评论区留言~~~ 一、在通知栏显示下载进度 利用GET方式读取数据有很多缺点比如1&#xff1a;无法端点续传 一旦中途失败只能重新获取 2&#xff1a;不是真正意义上的下载操作 无法设置参数 3&#xff1a;下载过程中无法在界面上上展示下…

NAFNet(ECCV 2022)-图像修复论文解读

文章目录解决问题算法背景Simple BaselinePlain Block归一化激活函数Attention机制总结NAFNetSimpleGate替换GELUSCA替换CA总结实验应用RGB图像去噪图像去模糊RAW图像去噪结论论文: 《Simple Baselines for Image Restoration》github: https://github.com/megvii-research/NAF…

同事:这个页面的逻辑没什么能复用的,不抽组件也没什么影响吧?

前言 最近在维护同事的一个项目时&#xff0c;发现有不少单个vue文件一千余行&#xff0c;同一个文件上有倒计时、有输入信息的表单&#xff1b; 当时我就在想&#xff1a;是不是策划经常改需求或者排期紧急&#xff0c;所以没抽组件呢。 沟通过程 以下同事称为阿A 我&#…

【附源码】计算机毕业设计JAVA家庭理财管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis Maven Vue 等等组成&#xff0c;B/…

Java基础—Document类型的变化

Document类型的变化 Document类型的变化中唯一与命名空间无关的方法是importNode()。这个方法的用途是从一个文档中取得一个节点&#xff0c;然后将其导入到另一个文档&#xff0c;使其成为这个文档结构的一部分。需要注意的是&#xff0c;每个节点都有一个ownerDocument属性&…

G1D13-Apt论文阅读fraudgitKGbookrce33-36php环境搭建

一、APT论文 今天终于把6个模型论文和一篇综述读完了&#xff01;&#xff01;&#xff01; 今天主要读了一篇论文写了个总表。发现之前读的论文都忘了&#xff0c;所以 明天要复习一下模型&#xff0c;记录在文档中&#xff0c;并完善模型对比的总表&#xff0c;并且把代码下…

[附源码]java毕业设计基于web的建筑合同管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

嵌入式FreeRTOS学习九,任务链表的构成,TICK时间中断和任务状态切换调度

一. tskTaskControlBlock 函数结构体 在tskTaskControlBlock 任务控制块结构体中&#xff0c;其中有任务状态链表和事件链表两个链表成员&#xff0c;首先介绍任务状态链表这个结构&#xff0c;这个链表通常用于管理不同状态的任务&#xff1b;通常&#xff0c;操作系统任务有…

CPU、内存、磁盘性能监控

CPU监控 网络由设备、服务器、路由器、交换机和其他网络组件组成。CPU 是网络中所有硬件设备的组成部分。它负责设备的稳定性和性能。企业严重依赖网络&#xff0c;企业硬件的处理能力决定了网络的容量。随着 CPU 功能和硬件的快速发展&#xff0c;组织必须规划其容量并监控其…

成功上岸,刚转行自学Python的小姑娘,每个月入1W+......

我是一名2020年毕业的本科生&#xff0c;大学学的专业是机械设计制造及其自动化。 在大学期间&#xff0c;觉得机械专业实在枯燥无味&#xff0c;没有一点点成就感&#xff0c;每天就是画图纸&#xff0c;测量零件&#xff0c;计算数据&#xff0c;一切都是纸上谈兵。但凡有因…

甲氧基PEG多巴胺DPA-mPEG,Dopamine-mPEG,PEG化的多巴胺具有良好的水溶性

英文名称&#xff1a;mPEG-DPA mPEG-Dopamine 中文名称&#xff1a;甲氧基-聚乙二醇-多巴胺 分子量&#xff1a;1k&#xff0c;2k&#xff0c;3.4k&#xff0c;5k&#xff0c;10k 产地&#xff1a;广州 品牌&#xff1a;为华生物 存储条件&#xff1a;≤-4℃低温干燥保存…

Netty-实验

Netty应用实例-群聊系统 实例要求&#xff1a; &#xff08;1&#xff09;编写一个Netty群聊系统&#xff0c;实现服务端和客户端之间的数据简单通讯&#xff08;非阻塞&#xff09; &#xff08;2&#xff09;实现多人群聊 &#xff08;3&#xff09;服务器端&#xff1a;可…

论文阅读笔记 | 三维目标检测——PointRCNN

如有错误&#xff0c;恳请指出。 文章目录1. 背景2. 网络结构2.1 Proposal Generation2.2 Proposal Refinement3. 实验部分3.1 kitti上的测评3.2 消融实验paper&#xff1a;《PointRCNN: 3D Object Proposal Generation and Detection from Point Cloud》文章比较复杂&#xff…

一文详解Redis企业版软件!

一、Redis企业版软件概述 Redis企业版软件&#xff08;Redis Enterprise&#xff09;是企业级的数据库软件&#xff0c;也是一款实时数据平台&#xff0c;为全球超过8500家知名企业提供实时数据服务。具有线性可扩展性、高可用性、持久性、备份和恢复、地理分布、分层内存访问…

WhatsApp群发系统-SendWS拓客系统功能后台介绍(五):WhatsApp筛号群发,群发超链

WhatsApp群发系统 基于WhatsApp进行群发功能&#xff0c;将品牌和产品推送给全世界各地的人们或者选择筛选好的用户&#xff0c;进行针对性的群发&#xff0c;提升了品牌和产品的影响力&#xff0c;让更多人了解认识品牌&#xff0c;帮助客户低成本实现WhatsApp营销精准拓客。…

windows和linux可以共用的端口连通性是否丢包测试工具paping

通常我们在系统无论是在windows还是linux&#xff0c;都会使用telnet命令来测试端口的连通性&#xff0c;但此命令只能测试是否通&#xff0c;无法测试是否有丢包或者是否有中断。paping这个工具就应用而生&#xff0c;它可以在多系统环境下进行像ping一样测试。 一、下载&…

【vscode】远程容器内开发python

一、环境 本人的远程开发环境&#xff1a; docker容器miniconda 常用的IDE&#xff1a; pyCharm专业版vsCodeRemote Development插件Python插件 由于pyCharm专业版要么花钱要么破解&#xff0c;我选择了vscode插件的方式&#xff0c;插件都是microsoft出品。 二、使用 服务…