📖React中的合成事件
type
status
date
slug
summary
tags
category
icon
password
Status
React自己实现了一套高效的事件注册、存储、分发和重用的逻辑,在DOM事件体系上做了很大改进,减少了内存消耗,简化事件逻辑,并最大程度解决了IE等浏览器的不兼容问题。
SyntheticEvent
React的合成事件SyntheticEvent,实际上就是React在自己内部实现的一套事件处理机制,它是浏览器原生事件的跨浏览器包装器。除了兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括
stopPropagation()
和preventDefault()
。合成事件与与浏览器的原生事件不同,也不会直接映射到原生事件,通常不要使用addEventListener
为已创建的DOM元素添加监听器,而应该直接使用React中定义的事件机制,并且在混用的情况下原生事件如果定义了阻止冒泡可能会阻止合成事件的执行。如果确实需要使用原生事件去处理需求,可以通过事件触发传递的SyntheticEvent
对象的nativeEvent
属性获得原生Event对象的引用。React中的事件有以下几个特点:
- React上注册的事件最终会绑定在
document
这个DOM上,而不是React组件对应的DOM。通过这种方式,可以减少内存开销,实际上是事件委托。
- React自己是实现了一套事件冒泡机制,React实现的Event对象和原生的Event对象不同,两者不可混用。
- React通过队列的方式,从触发事件的组件向父组件回溯,然后调用其JSX定义的callback。
- React通过对象池管理合成事件对象的创建和销毁,减少了垃圾的生成和新对象内存的分配,提高了性能。
对于每个
SyntheticEvent
对象都包含以下属性:React支持的合成事件一览,注意以下的事件处理函数在冒泡阶段被触发,如果需要注册捕获阶段的事件处理函数,则应为事件名添加
Capture
,比如处理捕获阶段的点击事件请使用onClickCapture
,而不是onClick
。一个简单的示例,同时绑定在一个DOM上的原生事件与React事件,因为原生事件阻止冒泡而导致React事件无法执行,同时我们也可以看到React传递的
event
并不是原生Event
对象的实例,而是React自行实现维护的一个event
对象。事件系统
简单来说,在挂载的时候,通过
listenerBank
把事件存起来了,触发的时候document
进行dispatchEvent
,找到触发事件的最深的一个节点,向上遍历拿到所有的callback放在eventQueue
,根据事件类型构建event
对象,遍历执行eventQueue
,不简单点说,我们可以查看一下React对于事件处理的源码实现,commit id
为4ab6305
,TAG
是React16.10.2
,在React17不再往document
上挂事件委托,而是挂到DOM容器上,目录结构都有了很大更改,我们还是依照React16,首先来看一下事件的处理流程。在
packages\react-dom\src\events\ReactBrowserEventEmitter.js
中就描述了上边的流程,并且还有相应的英文注释,使用google
翻译一下,这个太概述了,所以还是需要详细描述一下,在事件处理之前,我们编写的JSX
需要经过babel
的编译,创建虚拟DOM
,并处理组件props
,拿到事件类型和回调fn
等,之后便是事件注册、存储、合成、分发、执行阶段。Top-level delegation
用于捕获最原始的浏览器事件,它主要由ReactEventListener
负责,ReactEventListener
被注入后可以支持插件化的事件源,这一过程发生在主线程。
-
React
对事件进行规范化和重复数据删除,以解决浏览器的问题,这可以在工作线程中完成。
- 将这些本地事件(具有关联的顶级类型用来捕获它)转发到
EventPluginHub
,后者将询问插件是否要提取任何合成事件。
- 然后
EventPluginHub
将通过为每个事件添加dispatches
(引用该事件的侦听器和ID
的序列)来对其进行注释来进行处理。
- 再接着,
EventPluginHub
会调度分派事件。
事件注册
首先会调用
setInitialDOMProperties()
判断是否在registrationNameModules
列表中,在的话便注册事件,列表包含了可以注册的事件。如果事件名合法而且是一个函数的时候,就会调用
ensureListeningTo()
方法注册事件。ensureListeningTo
会判断rootContainerElement
是否为document
或是Fragment
,如果是则直接传递给listenTo
,如果不是则通过ownerDocument
来获取其根节点,对于ownerDocument
属性,定义是这样的,ownerDocument
可返回某元素的根元素,在HTML
中HTML
文档本身是元素的根元素,所以可以说明其实大部分的事件都是注册在document
上面的,之后便是调用listenTo
方法实际注册。在
listenTo()
方法中比较重要的就是registrationNameDependencies
的概念,对于不同的事件,React
会同时绑定多个事件来达到统一的效果。此外listenTo()
方法还默认将事件通过trapBubbledEvent
绑定,将onBlur
、onFocus
、onScroll
等事件通过trapCapturedEvent
绑定,因为这些事件没有冒泡行为,invalid
、submit
、reset
事件以及媒体等事件绑定到当前DOM
上。之后就是熟知的对事件的绑定,以事件冒泡
trapBubbledEvent()
为例来描述处理流程,可以看到其调用了trapEventForPluginEventSystem
方法。可以看到
React
将事件分成了三类,优先级由低到高: * DiscreteEvent
离散事件,例如blur
、focus
、 click
、 submit
、 touchStart
,这些事件都是离散触发的。 * UserBlockingEvent
用户阻塞事件,例如touchMove
、mouseMove
、scroll
、drag
、dragOver
等等,这些事件会阻塞用户的交互。 * ContinuousEvent
连续事件,例如load
、error
、loadStart
、abort
、animationEnd
,这个优先级最高,也就是说它们应该是立即同步执行的,这就是Continuous
的意义,是持续地执行,不能被打断。此外
React
将事件系统用到了Fiber
架构里,Fiber
中将任务分成了5
大类,对应不同的优先级,那么三大类的事件系统和五大类的Fiber
任务系统的对应关系如下。 * Immediate
: 此类任务会同步执行,或者说马上执行且不能中断,ContinuousEvent
便属于此类。 * UserBlocking
: 此类任务一般是用户交互的结果,需要及时得到反馈,DiscreteEvent
与UserBlockingEvent
都属于此类。 * Normal
: 此类任务是应对那些不需要立即感受到反馈的任务,比如网络请求。 * Low
: 此类任务可以延后处理,但最终应该得到执行,例如分析通知。 * Idle
: 此类任务的定义为没有必要做的任务。回到
trapEventForPluginEventSystem
,实际上在这三类事件,他们最终都会有统一的触发函数dispatchEvent
,只不过在dispatch
之前会需要进行一些特殊的处理。到达最终的事件注册,实际上就是在
document
上注册了各种事件。事件存储
让我们回到上边的
listenToTopLevel
方法中的listeningSet.add(topLevelType)
,即是将事件添加到注册到事件列表对象中,即将DOM
节点和对应的事件保存到Weak Map
对象中,具体来说就是DOM
节点作为键名,事件对象的Set
作为键值,这里的数据集合有自己的名字叫做EventPluginHub
,当然在这里最理想的情况会是使用WeakMap
进行存储,不支持则使用Map
对象,使用WeakMap
主要是考虑到WeakMaps
保持了对键名所引用的对象的弱引用,不用担心内存泄漏问题,WeakMaps
应用的典型场合就是DOM
节点作为键名。事件合成
首先来看看
handleTopLevel
的逻辑,handleTopLevel
主要是缓存祖先元素,避免事件触发后找不到祖先元素报错,接下来就进入runExtractedPluginEventsInBatch
方法。在
runExtractedPluginEventsInBatch
中extractPluginEvents
用于通过不同的插件合成事件events
,而runEventsInBatch
则是完成事件的触发。在
extractPluginEvents
中遍历所有插件的extractEvents
方法合成事件,如果这个插件适合于这个events
则返回它,否则返回null
。默认的有5
种插件SimpleEventPlugin
、EnterLeaveEventPlugin
、ChangeEventPlugin
、SelectEventPlugin
、BeforeInputEventPlugin
。不同的事件类型会有不同的合成事件基类,然后再通过
EventConstructor.getPooled
生成事件,accumulateTwoPhaseDispatches
用于获取事件回调函数,最终调的是getListener
方法。
为了避免频繁创建和释放事件对象导致性能损耗(对象创建和垃圾回收),React
使用一个事件池来负责管理事件对象(在React17
中不再使用事件池机制),使用完的事件对象会放回池中,以备后续的复用,也就意味着事件处理器同步执行完后,SyntheticEvent
属性就会马上被回收,不能访问了,也就是事件中的e
不能用了,如果要用的话,可以通过一下两种方式: * 使用e.persist()
,告诉React
不要回收对象池,在React17
依旧可以调用只是没有实际作用。 * 使用e. nativeEvent
,因为它是持久引用的。事件分发
事件分发就是遍历找到当前元素及父元素所有绑定的事件,将所有的事件放到
event._dispachListeners
队列中,以备后续的执行。事件执行
执行事件队列用到的方法是
runEventsInBatch
,遍历执行executeDispatchesInOrder
方法,通过executeDispatch
执行调度,最终执行回调函数是通过invokeGuardedCallbackAndCatchFirstError
方法。- Twikoo