react div onclick叠加_深入学习 React 合成事件
翁斌斌,微医云前端工程师,在程序员的修炼道路上永不止步。
以下分析基于React, ReactDOM 16.13.1版本
提出问题
我们借鉴一个比较典型的案例开始来分析React
事件
export
从上面的代码里我们不难看出我们想要做一个点击某一个按钮来展示一个模态框,并且在点击除了模态框区域以外的位置希望能够关闭这个模态框。 但是实际运行结果和我们所想的完全不一样,点击了button按钮并没有任何反应,这就需要从React
的合成事件说起了,让我们分析完React
的合成事件 后能够完全的来解答这个问题。
demo地址:https://codesandbox.io/s/event-uww15?file=/src/App.tsx:0-690
合成事件的特性
React
自行实现了一套事件系统,主要特性有以下
- 自行实现了一套事件捕获到事件冒泡的逻辑, 抹平各个浏览器之前的兼容性问题。
- 使用对象池来管理合成事件对象的创建和销毁,可以减少垃圾回收次数,防止内存抖动。
- 事件只在document上绑定,并且每种事件只绑定一次,减少内存开销。
首先我们先抛开上面那个按钮,用下面这个十分简单的案例来了解React
的事件使用。
function App() {
上面的代码运行后,会在控制台中分别打印出,button, h1, div
三个dom节点,我们来研究一下他是如何工作的。
事件绑定
首先来确认事件是如何绑定到dom节点
上的,我们知道App组件
内的jsx代码
会通过React.CreateElement
函数返回jsx对象
,其中我们的onClick事件
是储存在每一个jsx对象
的props属性
内,通过一系列方法得知在React
在reconciliation
阶段中会把jsx对象
转换为fiber对象
,这里有一个方法叫做completeWork
,
function completeWork(current, workInProgress, renderExpirationTime) {
这个函数内通过createInstance
创建dom实例,并且调用finalizeInitialChildren
函数,在finalizeInitialChildren
函数中会把props设置到真实的dom节点上,这里如果遇到类似onClick,onChange
的props时,会触发事件绑定的逻辑。
// 进行事件绑定
在ensureListeningTo
函数中会通过实际触发事件的节点,去寻找到它的document
节点,并且调用legacyListenToEvent
函数来进行事件绑定
function legacyListenToEvent(registrationName, mountAt) {
registrationNameDependencies
数据结构如下
在legacyListenToEvent
函数中首先通过获取document
节点上监听的事件名称Map对象,然后去通过绑定在jsx上的事件名称,例如onClick来获取到真实的事件名称,例如click
,依次进行legacyListenToTopLevelEvent
方法的调用
function legacyListenToTopLevelEvent(topLevelType, mountAt, listenerMap) {
legacyListenToTopLevelEvent
函数做了以下两件事
- 是否在document上已经绑定过原始事件名,已经绑定过则直接退出,未绑定则绑定结束以后把事件名称设置到Map对象上,再下一次绑定相同的事件时直接跳过。
- 根据事件是否能冒泡来来进行捕获阶段的绑定或者冒泡阶段的绑定。
function trapEventForPluginEventSystem(container, topLevelType, capture) {
到目前为止我们已经拿到了真实的事件名称和绑定在事件的哪个阶段,剩下就还有一个监听事件本身了,这一步会在trapEventForPluginEventSystem
函数内被获取到,他会通过事件的优先级来获取不同的监听事件,这部分会和调度方面有相关,我们只需要知道最终实际绑定的都是dispatchEvent
这个监听事件,然后调用浏览器的addEventListener
事件来绑定上dispatchEvent
函数
到此为止事件的绑定暂时告一段落了,从上面能得出几个结论。
- 事件都是绑定在document上的。
- jsx中的事件名称会经过处理,处理后的事件名称才会被绑定,例如onClick会使用click这个名称来绑定。
- 不管用什么事件来绑定, 他们的监听事件并不是传入jsx的事件函数,而是会根据事件的优先级来绑定
dispatchDiscreteEvent,dispatchUserBlockingUpdate或者dispatchEvent
三个监听函数之一,但是最终在触发事件调用的还是dispatchEvent
事件。
事件触发
从事件绑定得知我们点击的button按钮的时候,触发的回调函数并不是实际的回调函数,而是dispatchEvent
函数, 所以我们通常会有几个疑问
- 它是怎么获取到用户事件的回调函数的?
- 为什么在合成事件对象不能被保存下来,而需要调用特殊的函数才能保留?
- 合成事件是怎么创建出来的?
function dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, targetInst) {
接下来的分析中我们就来解决这几个问题,首先看到dispatchEvent
函数,忽略掉其他分支会发现实际调用的是dispatchEventForLegacyPluginEventSystem
函数, 他首先通过callbackBookkeepingPool
中获取一个bookKeeping
对象,然后调用handleTopLevel
函数,在调用结束的时候吧bookKeeping
对象放回到callbackBookkeepingPool
中,实现了内存复用。
bookKeeping对象的结构如图
// 忽略分支代码,只保留主流程
在handleTopLevel
函数内,通过首先把触发事件的节点如果是dom节点或者文字节点的话,那就把对应的fiber对象放入bookkeeping.ancestors
的数组内,接下去依次获取bookKeeping.ancestors
上的每一个fiber对象,通过runExtractedPluginEventsInBatch
函数来创建合成事件对象。
function runExtractedPluginEventsInBatch(topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags) {
在runExtractedPluginEventsInBatch
中会通过调用extractPluginEvents
函数,在这个函数内通过targetInst
这个fiber对象,从这个对象一直往上寻找,寻找有一样的事件绑定的节点,并且把他们的回调事件组合到合成事件对象上,这里先讨论事件触发的流程,所以先简单带过合成事件是如何生成的以及是如何去寻找到需要被触发的事件, 后面会详细的讲解合成事件,最后在拿到合成事件以后调用runEventsInBatch
函数
function runEventsInBatch(events) {
其中processingEventQueue
是多个事件列表,我们这只有一个事件队列,forEachAccumulated
它的目的是为了按照队列的顺序去执行多个事件,在我们的例子中其实就相当于executeDispatchesAndReleaseTopLevel(processingEventQueue)
,接下来就是调用到executeDispatchesAndRelease
,从名称就看出来他是首先执行事件,然后对事件对象进行释放
var executeDispatchesAndRelease =
代码很少,首先调用executeDispatchesInOrder
来传入合成事件,在里面按照顺序去执行合成事件对象上的回调函数,如果有多个回调函数,在执行每个回调函数的时候还会去判断event.isPropagationStopped()
的状态,之前有函数调用了合成事件的stopPropagation函数的话,就停止执行后续的回调,但是要注意的时候这里的dispatchListeners[i]
函数并不是用户传入的回调函数,而是经过包装的事件,这块会在合成事件的生成中介绍,在事件执行结束后React还会去根据用户是否调用了event.persist()
函数来决定是否保留这次的事件对象是否要回归事件池,如果未被调用,该事件对象上的状态会被重置,至此事件触发已经完毕。
合成事件的生成
从事件监听的流程中我们知道了合成事件是从extractPluginEvents
创建出来的,那么看一下extractPluginEvents
的代码
function extractPluginEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags) {
首先来了解一下plugins是个什么东西,由于React会服务于不同的平台,所以每个平台的事件会用插件的形式来注入到React中,例如浏览器就是ReactDOM中进行注入
injectEventPluginsByName({ SimpleEventPlugin: SimpleEventPlugin, EnterLeaveEventPlugin: EnterLeaveEventPlugin, ChangeEventPlugin: ChangeEventPlugin, SelectEventPlugin: SelectEventPlugin, BeforeInputEventPlugin: BeforeInputEventPlugin,});
injectEventPluginsByName
函数会通过一些操作把事件插件注册到plugins
对象上,数据结构如下
所以会依次遍历plugin
,调用plugin
上的extractEvents
函数来尝试是否能够生成出合成事件对象,在我们的例子中用的是click事件,那么它会进入到SimpleEventPlugin.extractEvents
函数
var SimpleEventPlugin = {
这个函数是通过topLevelType
的类型来获取合成事件的构造函数,例如代码中的SyntheticKeyboardEvent,SyntheticFocusEvent
等都是SyntheticEvent
的子类,在基础上附加了自己事件的特殊属性,我们的click事件会使用到SyntheticEvent
这个构造函数,然后通过getPooled
函数来创建或者从事件池中取出一个合成事件对象实例。然后在accumulateTwoPhaseDispatchesSingle
函数中,按照捕获到冒泡的顺序来获取所有的事件回调
function accumulateTwoPhaseDispatchesSingle(event) {
traverseTwoPhase
函数会从当前的fiber节点通过return属性,找到所有的是原生DOM节点的fiber对象,然后推入到列表中,我们的例子中就是[ButtonFiber, H1Fiber, DivFiber]
, 首先执行捕获阶段的循环,从后往前执行,接着从前往后执行冒泡的循环,对应了浏览器原始的事件触发流程,最后会往accumulateDirectionalDispatches
函数中传入当前执行的fiber和事件执行的阶段。
function listenerAtPhase(inst, event, propagationPhase) {
listenerAtPhase
中首先通过原生事件名和当前执行的阶段(捕获,还是冒泡)去再去获取对应的props事件名称(onClick,onClickCapture)
,然后通过React事件名称去fiber节点上获取到相应的事件回调函数,最后拼接在合成对象的_dispatchListeners
数组内,当全部节点运行结束以后_dispatchListeners
对象上就会有三个回调函数[handleButtonLog, handleH1Log, handleDivLog]
,这里的回调函数就是我们在组件内定义的真实事件的回调函数。
到此合成事件构造就完成了,主要做了三件事:
- 通过事件名称去选择合成事件的构造函数,
- 事件去获取到组件上事件绑定的回调函数设置到合成事件上的
_dispatchListeners
属性上,用于事件触发的时候去调用。 - 还有就是在初始化的时候去注入平台的事件插件。
事件解绑
通常我们写事件绑定的时候会在页面卸载的时候进行事件的解绑,但是在React中,框架本身由于只会在document上进行每种事件最多一次的绑定,所以并不会进行事件的解绑。
批量更新
当然如果我们使用React提供的事件,而不是使用我们自己绑定的原生事件除了会进行事件委托以外还有什么优势呢? 再来看一个例子
export
在线demo地址:https://codesandbox.io/s/legacy-event-kjngx?file=/src/App.tsx:0-1109
首先点击第一个按钮,发现有两个update被打印出,意味着被render了两次。
点击第二个按钮,只有一个update被打印出来。
会发现通过React事件内多次调用setState
,会自动合并多个setState
,但是在原生事件绑定上默认并不会进行合并多个setState
,那么有什么手段能解决这个问题呢?
- 通过
batchUpdate
函数来手动声明运行上下文。
() => {
在线demo地址:https://codesandbox.io/s/legacy-eventbatchupdate-smisq?file=/src/App.tsx:519-749
首先点击第一个按钮,只有一个update被打印出来。
点击第二个按钮,还是只有一个update被打印出来。
- 启用
concurrent mode
的情况。(目前不推荐,未来的方案)
import ReactDOM
在线demo地址:https://codesandbox.io/s/concurrentevent-9oxoi?file=/src/index.js:0-224
会发现不需要修改任何代码,只需要开启
concurrent mode
,就会自动进行setState
的合并。
首先点击第一个按钮,只有一个update被打印出来。
点击第二个按钮,还是只有一个update被打印出来。
React17中的事件改进
在最近发布的React17
版本中,对事件系统了一些改动,和16版本里面的实现有了一些区别,我们就来了解一下17中更新的点。
- 更改事件委托
- 首先第一个修改点就是更改了事件委托绑定节点,在16版本中,React都会把事件绑定到页面的document元素上,这在多个React版本共存的情况下就会虽然某个节点上的函数调用了
e.stopPropagation()
,但还是会导致另外一个React版本上绑定的事件没有被阻止触发,所以在17版本中会把事件绑定到render函数的节点上。
- 去除事件池
- 17版本中移除了
event pooling
,这是因为 React 在旧浏览器中重用了不同事件的事件对象,以提高性能,并将所有事件字段在它们之前设置为 null。在 React 16 及更早版本中,使用者必须调用e.persist()
才能正确的使用该事件,或者正确读取需要的属性。
- 对标浏览器
onScroll
事件不再冒泡,以防止出现常见的混淆。- React 的
onFocus
和onBlur
事件已在底层切换为原生的focusin
和focusout
事件。它们更接近 React 现有行为,有时还会提供额外的信息。 - 捕获事件(例如,
onClickCapture
)现在使用的是实际浏览器中的捕获监听器。
问题解答
现在让我们回到最开始的例子中,来看这个问题如何被修复
. 16版本修复方法一
(e: React.MouseEvent) => {
我们知道React事件绑定的时刻是在reconciliation
阶段,会在原生事件的绑定前,那么可以通过调用e.nativeEvent.stopImmediatePropagation()
; 来进行document后续事件的阻止。
在线demo地址:https://codesandbox.io/s/v16fixevent1-wb8m7
- 16版本修复方法二
window.addEventListener(
另外一个方法就是在16版本中事件会被绑定在document上,所以只要把原生事件绑定在window上,并且调用e.nativeEvent.stopPropagation()
;来阻止事件冒泡到window上即可修复。
在线demo地址:https://codesandbox.io/s/v16fixevent2-4e2b5
- React17版本修复方法
在17版本中React事件并不会绑定在document上,所以并不需要修改任何代码,即可修复这个问题。
在线demo地址:https://codesandbox.io/s/v17fixevent-wzsw5
总结
我们通过一个经典的例子入手,自顶而下来分析React源码中事件的实现方式,了解事件的设计思想,最后给出多种的解决方案,能够在繁杂的业务中挑选最合适的技术方案来进行实践。
react div onclick叠加_深入学习 React 合成事件相关推荐
- react中样式冲突_如何通过React中的样式使您的应用漂亮
react中样式冲突 by Vinh Le 由Vinh Le 如何通过React中的样式使您的应用漂亮 (How to make your apps pretty with styling in Re ...
- react 组件引用组件_自定位React组件
react 组件引用组件 While React has ways to break the hatch and directly manipulate the DOM there are very ...
- react项目开发步骤_成为专业React开发人员的31个步骤
react项目开发步骤 我为达到可雇用水平而进行的每个项目和课程. (Every single project and course I took to reach a hireable level. ...
- react 组件引用组件_动画化React组件
react 组件引用组件 So, you want to take your React components to the next level? Implementing animations c ...
- react 日期怎么格式化_手写React的Fiber架构,深入理解其原理
熟悉React的朋友都知道,React支持jsx语法,我们可以直接将HTML代码写到JS中间,然后渲染到页面上,我们写的HTML如果有更新的话,React还有虚拟DOM的对比,只更新变化的部分,而不重 ...
- react js 添加样式_如何在React JS Application中添加图像?
react js 添加样式 Hello! In this article, we will learn how to add images in React JS? I remember when I ...
- react 动态修改路由_升级到 React Router 4 并实现动态加载和模块热替换
这篇文章是升级到Webpack2坑--模块热替换失效页面不自动刷新?的后续.那篇文章主要说明了,升级到 Webpack 2 之后,通过升级webpack-dev-server和react-hot-lo ...
- react 统一字段验证_如何使用React的受控输入进行即时表单字段验证
react 统一字段验证 by Gosha Arinich 通过Gosha Arinich 如何使用React的受控输入进行即时表单字段验证 (How to use React's controlle ...
- bootstrap缩小后div互相叠加_纯 JS 实现放大缩小拖拽踩坑之旅
点击上方"前端公虾米",选择"置顶或者星标" 你的关注意义重大! 前言 最近团队需要做一个智能客服悬浮窗功能,需要支持拖动.放大缩小等功能,因为这个是全局插件, ...
最新文章
- 基于GA的TSP问题
- Spring Data JPA
- 让我们来开发一种更类似人脑的神经网络吧(五)
- C语言 float、double数据在内存中的存储方式
- Python 可以满足你任何 API 使用需求
- 这只拒绝内卷的 AI 狼火了!高智商却自暴自弃,不想抓羊只想躺
- Python中文编程
- Rust : wasm尝试 与wasmtime库
- C#实现百度AI-实时语音识别转写-附源码
- android 自定义控件实现3D画廊效果
- 当我再次看到你————中秋致Leslie
- 资源隔离的两种虚拟化技术——虚拟机容器 容器技术的资源隔离
- 2011中国 · 移动开发者大会侧记
- PS小技巧----1寸、2存照片制作
- 设置Button图片位置
- 开发一个在线Excel系统?SpreadJS让开发如此简单
- 微信开发者工具编译失败显示找不到icons中的图片
- 2021莆田六中一高考成绩查询入口,2021,我们来了 ——莆田六中2021届《青春•励志•圆梦》高三高考动员誓师大会...
- C语言if( x)的意思,c语言 if(!x)中条件!x是什么意思
- Grand Central Dispatch 基础教程:Part 1/2