深度分析React源码中的合成事件
热身准备
明确几个概念
在React@17.0.3版本中:
- 所有事件都是委托在
id = root的DOM元素中(网上很多说是在document中,17版本不是了); - 在应用中所有节点的事件监听其实都是在
id = root的DOM元素中触发; React自身实现了一套事件冒泡捕获机制;React实现了合成事件SyntheticEvent;React在17版本不再使用事件池了(网上很多说使用了对象池来管理合成事件对象的创建销毁,那是16版本及之前);- 事件一旦在
id = root的DOM元素中委托,其实是一直在触发的,只是没有绑定对应的回调函数;
事件系统角色划分
- 事件注册:
registerEvents; - 事件监听:
listenToAllSupportedEvents; - 事件合成:
SyntheticBaseEvent; - 事件派发:
dispatchEvent;
事件注册
事件注册是自执行的,也就是React自身进行调用的:
// 注册React事件
registerSimpleEvents();registerEvents$2();
registerEvents$1();
registerEvents$3();
registerEvents();
React事件就是在组件中调用的onClick这种写法的事件。上面分为5个函数写,主要是区分不同的事件注册逻辑,但是最后都会添加到allNativeEvents的Set数据结构中。
registerSimpleEvents
这里会注册大部分事件,它们在React被定义为顶级事件。
它们分为三类:
- 离散事件:
discreteEvent,常见的如:click, keyup, change; - 用户阻塞事件:
userBlocking,常见的如:dragEnter, mouseMove, scroll; - 连续事件:
continuous,常见的如:error, progress, load,; 它们的优先级排序:
0:离散事件, 1:用户阻塞事件, 2:连续事件
它们会注册冒泡和捕获阶段两个事件。
registerEvents$2
注册类似onMouseEnter,onMouseLeave单阶段事件,只注册冒泡阶段事件。
registerEvents$1
注册onChange相关事件,注册冒泡和捕获阶段两个事件。
registerEvents$3
注册onSelect相关事件,注册冒泡和捕获阶段两个事件。
registerEvents
注册onBeforeInput,onCompositionUpdate等相关事件,注册冒泡和捕获阶段两个事件。
事件监听
在React源码系列之二:React的渲染机制曾提到过,React在开始渲染前,会为应用创建一个fiberRoot作为应用的根节点。在创建fiberRoot还会做一件事,就是
listenToAllSupportedEvents(rootContainerElement);
从字面就能理解这个函数是做事件监听的,其中rootContainerElement参数就是应用中的id = root的DOM元素。
该函数主要遍历上面事件注册添加到allNativeEvents的事件,按照一定规则,区分冒泡阶段,捕获阶段,区分有无副作用进行监听,监听的api还是addEventListener:
// 监听冒泡阶段事件
function addEventBubbleListener(target, eventType, listener) {target.addEventListener(eventType, listener, false);return listener;
}
// 监听捕获阶段事件
function addEventCaptureListener(target, eventType, listener) {target.addEventListener(eventType, listener, true);return listener;
}
代码中的target就是id = root的DOM元素。
注意,上面监听的listener是一个事件派发器,并不是真实的浏览器事件或你写的事件回调函数。 不要搞混淆了。
事件派发
上面提到,事件一旦在id = root的DOM元素中委托,其实是一直在触发的,只是没有绑定对应的回调函数。
意思是,当我们把鼠标移入我们的应用页面中时,这时就在派发事件了,因为页面的DOM元素是有监听mousemove之类的事件的。
那问题来了,React是如何得知我们给事件绑定了回调函数并触发对应的回调函数的?
带着这个问题我们来研究下事件派发。
要讲事件派发,还得提下事件监听阶段监听的listener,它实际是下面这玩意:
function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {var eventPriority = getEventPriorityForPluginSystem(domEventName);var listenerWrapper;switch (eventPriority) {case DiscreteEvent:listenerWrapper = dispatchDiscreteEvent;break;case UserBlockingEvent:listenerWrapper = dispatchUserBlockingUpdate;break;case ContinuousEvent:default:listenerWrapper = dispatchEvent;break;}return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}
和事件注册一样,listener也分为dispatchDiscreteEvent, dispatchUserBlockingUpdate, dispatchEvent三种。它们之间的主要区别是执行优先级,还有discreteEvent涉及到要清除之前的discreteEvent问题,所以做了区分。但是它们最后都会调用dispatchEvent。相关参考视频讲解:进入学习
所以事件派发的角色应该是dispatchEvent
function dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {var allowReplay = true;allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;// 如果有离散事件正在执行,会排队,顺序执行if (allowReplay && hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(domEventName)) {domEventName, eventSystemFlags, targetContainer, nativeEvent);return;}// 尝试事件派发,如果成功,就不用执行下面的代码了var blockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent);// 尝试事件派发成功if (blockedOn === null) {if (allowReplay) {// 清除连续事件队列clearIfContinuousEvent(domEventName, nativeEvent);}return;}if (allowReplay) {if (isReplayableDiscreteEvent(domEventName)) {queueDiscreteEvent(blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent);return;}if (queueIfContinuousEvent(blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent)) {return;} clearIfContinuousEvent(domEventName, nativeEvent);} dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, null, targetContainer);
}
介绍下dispatchEvent的几个参数:
domEventName: DOM事件名称,如:click,不是onClick;eventSystemFlags:事件系统标记;targetContainer:id=root的DOM元素;nativeEvent:原生事件(来自addEventListener);
在attemptToDispatchEvent中, 根据nativeEvent.target找到真正触发事件的DOM元素,并根据DOM元素找到对应的fiber节点,判断fiber节点的类型以及是否已渲染来决定是否要派发事件。
在一系列判断通过后,就开始真正的事件处理了:
function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {// 获取触发事件的DOM元素var nativeEventTarget = getEventTarget(nativeEvent);// 初始化事件派发队列var dispatchQueue = [];// 合成事件extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);// 按事件派发队列执行事件派发processDispatchQueue(dispatchQueue, eventSystemFlags);
}
在extractEvents$5中会进行事件合成,放在下面单独讲。
在processDispatchQueue会根据事件阶段(冒泡或捕获)来决定是正序还是倒序遍历合成事件中的listeners。
接下来就比较简单了。 遍历listeners执行上面的listener。
合成事件
在合成事件中,会根据domEventName来决定使用哪种类型的合成事件。
以click为例,当我们点击页面的某个元素时,React会根据原生事件nativeEvent找到触发事件的DOM元素和对应的fiber节点。并以该节点为孩子节点往上查找,找到包括该节点及以上所有的click回调函数创建dispatchListener,并添加到listeners数组中。
// dispatchListener
{instance: instance,// 事件所在的fiber节点listener: listener,// 事件回调函数currentTarget: currentTarget// 事件对应的DOM元素}
当向上查找完成后,会基于click类型的合成事件类创建事件
// 创建合成事件实例
var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
// 事件派发队列添加事件
dispatchQueue.push({event: _event, // 合成事件实例listeners: _listeners// 同类型事件的集合数组
});
看下SyntheticEventCtor
// Interface根据事件类型有所不同
function createSyntheticEvent(Interface) {// 合成事件构造函数function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {this._reactName = reactName;this._targetInst = targetInst;this.type = reactEventType;this.nativeEvent = nativeEvent;this.target = nativeEventTarget;this.currentTarget = null;// React根据不同事件类型写了对应的属性接口,这里基于接口将原生事件上的属性clone到构造函数中for (var _propName in Interface) {... }var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;if (defaultPrevented) {this.isDefaultPrevented = functionThatReturnsTrue;} else {this.isDefaultPrevented = functionThatReturnsFalse;}this.isPropagationStopped = functionThatReturnsFalse;return this;}_assign(SyntheticBaseEvent.prototype, {// 阻止默认事件preventDefault: function () {...},// 阻止捕获和冒泡阶段中当前事件的进一步传播stopPropagation: function () {...},// 合成事件不使用对象池了,这个事件是空的,没有意义,保存是为了向下兼容不报错。persist: function () {},isPersistent: functionThatReturnsTrue});return SyntheticBaseEvent;
}
看到这里,我们基本能弄明白合成事件是个什么东西了。
React合成事件是将同类型的事件找出来,基于这个类型的事件,React通过代码定义好的类型事件的接口和原生事件创建相应的合成事件实例,并重写了preventDefault和stopPropagation方法。
这样,同类型的事件会复用同一个合成事件实例对象,节省了单独为每一个事件创建事件实例对象的开销,这就是事件的合成。
捕获和冒泡
事件派发分为两个阶段执行, 捕获阶段和冒泡阶段。
在上面事件合成中讲过,React会根据事件触发的fiber节点向上查找,将上面的同类型事件添加到队列中,这样天然就有了一个冒泡的顺序,从最底层向上冒泡。如果倒序过来遍历就是捕获的顺序。
所以,React实现冒泡和捕获就很简单了,只需要根据事件派发的阶段,判断是冒泡阶段还是捕获阶段来决定是正序遍历listeners还是倒序遍历就行了。
总结
说是讲React的合成事件,实际上讲了React的事件系统。做下总结:
React的事件系统分为几个部分:
- 事件注册;* 事件监听;* 事件合成;* 事件派发; 事件系统流程:1.在
React代码执行时,内部会自动执行事件的注册;2.第一次渲染,创建fiberRoot时,会进行事件的监听,所有的事件通过addEventListener委托在id=root的DOM元素上进行监听;3.在我们触发事件时,会进行事件合成,同类型事件复用一个合成事件类实例对象;4.最后进行事件的派发,执行我们代码中的事件回调函数;当然,由于篇幅问题,这里也是对React事件系统的一个精简剖析,可能忽略了一些地方,欢迎指正。
看完这篇文章, 我们可以弄明白下面这几个问题:
1.React事件委托在哪?
2.React合成事件是什么?
3.React合成事件是怎么实现的?
4.React是怎么实现冒泡和捕获的?
5.React合成事件是使用的原生事件吗?
6.React事件系统分为哪几个部分?
最后
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:




文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取
相关文章:
深度分析React源码中的合成事件
热身准备 明确几个概念 在React17.0.3版本中: 所有事件都是委托在id root的DOM元素中(网上很多说是在document中,17版本不是了);在应用中所有节点的事件监听其实都是在id root的DOM元素中触发;React自…...
17.微服务SpringCloud
一、基本概念 Spring Cloud 被称为构建分布式微服务系统的“全家桶”,它并不是某一门技术,而是一系列微服务解决方案或框架的有序集合。它将市面上成熟的、经过验证的微服务框架整合起来,并通过 Spring Boot 的思想进行再封装,屏蔽…...
Java基础面试题——JavaWeb专题
文章目录1.HTTP响应码有哪些2.Forward和Redirect的区别?3.Get和Post请求的区别4.介绍下OSI七层和TCP/IP四层的关系5.说说TCP和UDP的区别6. 说下HTTP和HTTPS的区别7.说下HTTP、TCP、Socket的关系是什么?8. 说下HTTP的长链接和短连接的区别9.TCP原理10. Co…...
MySql数据库约束
概述、目的 概念:约束是作用于表中字段上的规则,用于限制存储在表中的数据。 目的:保证数据库中数据的正确性、有效性和完整性。 分类: 约束描述关键字非空约束限制该字段的数据不能为nullNOT NULL唯一约束保证该字段的所有数据都…...
TripleCross:一款功能强大的Linux eBPF安全研究工具
关于TripleCross TripleCross是一款功能强大的Linux eBPF安全研究工具,该工具提供了后门、C2、代码库注入、执行劫持、持久化和隐蔽执行等功能。 功能介绍 1、使用一个代码库注入模块通过往进程的虚拟内存中写入命令来执行恶意代码; 2、提供了一个行劫…...
2023最牛教程,手把手教你成为年薪30W的测试开发
随着互联网行业的高速发展,快速高质量的产品版本迭代成为企业始终立于不败之地的迫切需求,而在短期迭代的快节奏中,传统测试工作面对更大压力,无法持续提供高效率高质量的人力支撑,所以越来越多的企业需要技术更为全面…...
“深度学习”学习日记。--ImageNet、VGG、ResNet
2023.2.14 一、小历史: 在2012年的ILSVRC(ImageNet Large Scale Visual Recognitoin Chanllege),基于深度学习的方法AlexNet 以绝对优势获胜并且他颠覆了以前的图片识别方法,此后深度学习方法一直活跃在这个舞台。 二、ImageNet࿱…...
关于APP下载量提升的技巧
关于APP应用下载量提升,很多人都不是很了解。今天厦门巨神峰小编给大家说下关于APP下载量提升的几个技巧。 一、抓住流行趋势,提升APP下载量 1、利用社交媒体进行推广。社交媒体是当下最流行的推广手段,可以有效的将APP的消息传播到更多的用…...
以“大数据”赋能产业链精准招商
随着我国产业发展的不断迭代升级,传统招商模式的不足逐步凸显,侧重土地与税费优惠的同质化竞争招商以及来者不拒的无门槛型招商已经遏制了区域产业的发展,导致各产业园区很难形成产业集聚及持续的吸引力。在这样的大环境下,产业…...
内存泄漏检测组件 -- hook
目录 hook malloc与free出现的问题 builtin_return_address(N) C/CLinux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂 hook malloc与free出现的问题 #define _GNU_SOURCE #include <stdio.h> #include <dlfcn.h> #include <stdlib.h> /****…...
Diffusion model(三): 公式结论
接上文 Diffusion model(一): 公式推导详解 这一节主要总结之前文章的公式结论 1. 已知x0x_{0}x0时,sample过程的均值和方差 q(xt−1∣xt,x0)(xt−1;μ~(xt,x0),β~tI)\begin{aligned} q(x_{t-1}|x_{t}, x_{0}) \mathcal(x_{t-1}; \tilde{\mu}(x_{t}, x_{0}),…...
Angular笔记(二)组件
组件包括: HTML 模板: 声明页面渲染的内容TypeScript 类: 定义行为CSS 选择器: 定义组件在模板中的使用方式(可选)要应用在模板上的 CSS 样式 一、 创建组件: 使用 Angular CLI 创建一个组件 ng generate component <component-name>…...
微信小程序|基于小程序+C#制作一个超酷的个人简历
你还在用以前的方式投简历吗?趁着金三银四来临之际,跟随此文使用小程序制作一个便携超酷的个人简历,高调炫技,愉快的收offer吧! 一、小程序...
华为OD机试 - 最快到达医院的方法(Java JS Python)
题目描述 新型冠状病毒疫情的肆虐,使得家在武汉的大壮不得不思考自己家和附近定点医院的具体情况。 经过一番调查,大壮明白了距离自己家最近的定点医院有两家。其中: 医院A和自己的距离是X公里医院B和自己的距离是Y公里由于武汉封城,公交停运,私家车不能上路,交通十分不…...
92.【SpringCloud NetFilx】
SpringCloud(一)、这个阶段该如何学习?1.微服务介绍2.面试常见问题(二)、微服务概述1.什么是微服务?2. 微服务与微服务架构(1).微服务(2).微服务架构⭐(3). 微服务优缺点(4). 微服务技术栈有那些?(5). 为什么选择SpringCloud作为微服务架构(三)、SpringCloud入门概…...
[ahk]如何载入Scite的会话Session文件
加载session文件的AutoHotkey代码:oSciTE : ComObjActive("SciTE4AHK.Application") messageloadsession:d:\\ddd\\2023-2-15SciTE.session oSciTE.SendDirectorMsg(message)存储session文件的AutoHotkey代码:messagesavesession:d:\\ddd\\123…...
MyISAM和InnoDb的区别
MySQL 5.0以后的版本默认的存储引擎为InnoDb,之前是MyISAM。 现在说说两者的区别: 1.数据存储结构的不同 MyISAM存储文件:.MYD(存储表数据),.MYI(存储表结构),.FRM(存储表结构) InnoDb存储文件: .FRM(存储表结构)&am…...
K8s管理应用生命周期-Deployment篇
在k8s中部署应用程序流程 1、使用Deployment部署Java应用 kubectl create deployment web --imageXXX/java-demokubectl get deployment,pods 2、使用Service发布Pod kubectl expose deployment web --port80 --typeNodePort --target-port8080 --namewebkubectl get servic…...
类和对象(下)(二)
类和对象(下)(二)1.友元1.1友元函数1.2友元类2.内部类3.拷贝对象时的一些编译器优化(vs2022)🌟🌟hello,各位读者大大们你们好呀🌟🌟 🚀…...
MapBox-draw绘制插件的使用教程(含修改样式和方法封装)
mapbox-draw插件是官方推荐的用于支持在mapbox地图中绘制图形的插件库。好像并不是由官方编写的,但是官方觉得其好用就直接推荐大家也使用了,我用了2天感觉下来还是觉得很鸡肋。对于开发者来讲自由修改的程度不是很高。这篇文章简单说一下对于mapbox-draw的使用和修改。 第一…...
(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)
题目:3442. 奇偶频次间的最大差值 I 思路 :哈希,时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况,哈希表这里用数组即可实现。 C版本: class Solution { public:int maxDifference(string s) {int a[26]…...
变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析
一、变量声明设计:let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性,这种设计体现了语言的核心哲学。以下是深度解析: 1.1 设计理念剖析 安全优先原则:默认不可变强制开发者明确声明意图 let x 5; …...
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以? 在 Golang 的面试中,map 类型的使用是一个常见的考点,其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...
css的定位(position)详解:相对定位 绝对定位 固定定位
在 CSS 中,元素的定位通过 position 属性控制,共有 5 种定位模式:static(静态定位)、relative(相对定位)、absolute(绝对定位)、fixed(固定定位)和…...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...
前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
Linux部署私有文件管理系统MinIO
最近需要用到一个文件管理服务,但是又不想花钱,所以就想着自己搭建一个,刚好我们用的一个开源框架已经集成了MinIO,所以就选了这个 我这边对文件服务性能要求不是太高,单机版就可以 安装非常简单,几个命令就…...
HybridVLA——让单一LLM同时具备扩散和自回归动作预测能力:训练时既扩散也回归,但推理时则扩散
前言 如上一篇文章《dexcap升级版之DexWild》中的前言部分所说,在叠衣服的过程中,我会带着团队对比各种模型、方法、策略,毕竟针对各个场景始终寻找更优的解决方案,是我个人和我司「七月在线」的职责之一 且个人认为,…...
上位机开发过程中的设计模式体会(1):工厂方法模式、单例模式和生成器模式
简介 在我的 QT/C 开发工作中,合理运用设计模式极大地提高了代码的可维护性和可扩展性。本文将分享我在实际项目中应用的三种创造型模式:工厂方法模式、单例模式和生成器模式。 1. 工厂模式 (Factory Pattern) 应用场景 在我的 QT 项目中曾经有一个需…...
《信号与系统》第 6 章 信号与系统的时域和频域特性
目录 6.0 引言 6.1 傅里叶变换的模和相位表示 6.2 线性时不变系统频率响应的模和相位表示 6.2.1 线性与非线性相位 6.2.2 群时延 6.2.3 对数模和相位图 6.3 理想频率选择性滤波器的时域特性 6.4 非理想滤波器的时域和频域特性讨论 6.5 一阶与二阶连续时间系统 6.5.1 …...
