这篇的事件注册和事件触发最好结合着事件系统0去看。
1. 事件池
上回提到,React为了避免垃圾回收,因而引入了事件池的概念,从而防止事件会被频繁的创建和回收。
从本质上来讲,事件池是React提供的一种优化方式,将所有的合成事件都放到事件池内统一管理,同时不同类型的合成事件对应不同的事件池。
拓展一个小问题:合成对象e如何持久化?
<input onChange={(e) => {
console.log(e.target) //<input>
setTimeout(() => {
console.log(e.target) //null
})
}} />
上面代码中,按理来说两处e.target应该一致,然而第二个e.target只能打印null,为什么呢?
这是因为每次在派发事件中,React都会从事件池中判断,是否能够复用,当派发完成时,就会将函数的属性置成null,也就是会清空对应的属性,所以setTimeout会打印出null的原因。
我们需要通过e.persistent() 来实现对象的持久化,可以使得onChange不进入事件池,因此也就不会被清空销毁,得以保留e的值。
2. 事件绑定(事件注册)
我们知道react中所有事件都是模拟的,那么react如何将模拟的事件进行绑定呢?
2.1 事件插件
在React中,所有的事件都是通过插件来进行统一处理,但并非是同一个插件,因为每个事件的处理逻辑、事件源都不同,所以会有多个事件插件。
如:onClick对应SimpleEventPlugin、onChange对应ChangeEventPlugin
这里介绍三个比较典型的插件:
- SimpleEventPlugin: 这个插件比较通用,大多数方法都是通过此插件处理,如:click、input、focus等,与原生事件一一对应,所以这类事件比较好处理
- EnterLeaveEventPlugin: onMouseEnter和onMouseLeave依靠于mouseout和mouseover原生事件,这样可以在document上面进行委托监听
- ChangeEventPlugin: 在React中,onChange比较特殊,它是React的一个自定义的事件,它依赖8种原生事件来模拟onChange事件
2.2 事件绑定
- 在React中,首先将虚拟dom转化成fiber,在fiber中的props如果是合成事件(如:onClick),就会按照独立的处理逻辑,单独处理
- 然后根据合成事件的事件类型,寻找对应的原生事件
- 之后会判断事件类型,大多数事件(onClick)都是走的冒泡逻辑,少部分事件(如:onScroll)会走捕获逻辑
- 最后会调用trapEventForPluginEventSystem函数,绑定在document上,实现统一处理函数(dispatchEvent函数)
3. 事件触发
- 所有的事件首先通过dispatchEvent函数处理,然后进行批量更新
- 然后根据事件源event找到与之匹配的DOM元素fiber,进入插件中的extractEvents,然后遍历,得到最终的一个队列,这个队列就是React用来模拟的事件过程
- 最终走向runEventsInBatch,进行批量执行事件队列,完成整个触发流程。如果发现有阻止冒泡的情况,则会跳出循环,重置事件源,再放回到事件池中,完成流程
4. v17/18事件系统
4.1 事件绑定
首先,在v17版本中,将顶层事件调整到container上,这样做的目的主要是为了:兼容性和跨平台,可以兼容多个版本,非常有利于微前端(微前端会对应多个系统,存在对应多个react版本的问题)
同时在React v16中,React执行大多数事件都会调用documnet.addEventListener(), 而在v17中,在底层中调用rootNode.addEventListener()
4.2 取消事件池
简单的说,就是没啥用,也没有对应的性能提高,所以就没了,但去除事件池后,自然也不存在持久化的问题,所以在setTimeout可以获得对应的事件源。
4.3 捕获事件
v16中不存在真正的捕获事件,是冒泡到document后模拟出来的,而v17/18真正存在。详情还是看事件系统0里有讲。