React源码解析(四):事件系统
在前面三篇文章中,我们阐述了react组件的构成与生命周期,setState的机制。这次我们来谈谈React的事件处理。
1.原生事件系统
我们通常监听真实DOM。举🌰来说,我们想监听按钮的点击事件,那么我们在按钮DOM上绑定事件和对应的回调函数即可。 遗憾的是若页面复杂且事件处理频率高,那么对网页性能是个考验。
2.React事件系统
react的事件处理再眼花缭乱终究还是要回归原生的事件系统,但它做的封装却很优雅。我们直接上结论:
- React实现了SyntheticEvent层处理事件
什么意思呢?详细来说,React并不像原生事件一样将事件和DOM一一对应,而是将所有的事件都绑定在网页的document,通过统一的事件监听器处理并分发,找到对应的回调函数并执行。按照官方文档的说法,事件处理程序将传递SyntheticEvent的实例,那么接下来我们一探SyntheticEvent的究竟。
3.SyntheticEvent
1.事件注册
上文说到,既然React对事件统一进行处理,那么肯定需要先注册程序员写的事件触发函数吧?那么这个过程是在哪里执行的呢?因为我们是把事件"绑定"在"组件DOM"上,例如一个点击事件:
<Component onClick={this.handleClick}/>
其实在这个组件挂载的时候,React就已经开始通过mountCompoent
内部的_updateDOMProperties
方法进行事件处理了。在这个方法中,执行的是enqueuePutListener
方法去注册事件:

顺藤摸瓜,listenTo
方法关键调用了以下两个函数:
- trapBubbledEvent
- trapCapturedEvent
熟悉原生事件系统的读者从英文翻译就能知道,两个函数是用来处理事件捕获和事件冒泡的。具体处理逻辑不分析,我们直接看这两个函数内部:

上述代码中的target
也就是document
,也看到了熟悉的document.addEventListener
和document.removeEventListener
。正是这样统一的事件绑定减少了内存的开销。
2.事件存储
我们写的事件回调函数注册完毕后需要存储起来,以便触发时进行回调。存储的入口是EventPluginHub.putListener
函数:

可见所有的回调函数都以二维数组的形式存储在listenerBank
中,根据组件对应的key
来进行管理。
3.事件分发
事件注册和事件存储我们已经清楚了,现在我们看下当事件触发时,React是如何进行事件分发和找到对应回调函数并执行的。分发入口在ReactDOMEventListener.js
的handleTopLevelImpl
:

上述代码我们理清了流程:因为事件回调函数执行后可能导致DOM结构的变化,那么React先将当前的结构以数组的形式存储起来,依次遍历执行。
上述函数的_handleTopLevel
最终对回调函数进行处理,看下源码:

代码中出现了新角色:EventPluginHub.extractEvents
。查阅相关资料,得知extractEvents
方法是用于合成事件的,也就是根据事件类型的不同,合成不同的跨浏览器的SyntheticEvent
对象的实例,比如SyntheticClickEvent
。而EventPluginHub
顾名思义是React进行合成事件时所用的工具插件:

可以看到对于不同的事件,React将使用不同的功能插件,这些插件都是通过依赖注入的方式进入内部使用的。React合成事件的过程非常繁琐,但可以概括出extractEvents
函数内部主要是通过switch
函数区分事件类型并调用不同的插件进行处理从而生成SyntheticEvent
实例。有兴趣的同学可以自行了解。
4.事件处理
React处理事件的思想与处理setState
的思想类似,都是采用批处理的方法。在上面handleTopLevel
方法中我们看到最后执行了runEventQueueInBatch
方法:
//事件进入队列
EventPluginHub.enqueueEvents(events);
//...
EventPluginHub.processEventQueue(false);
看下processEventQueue
:

上述代码遍历队列中的事件,并进入executeDispatchesAndReleaseSimulated
:
event.constructor.release(event);
这行代码将React的合成事件release掉,减少内存开销。事件处理的核心入口在executeDispatchesInOrder
:
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;
executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
重要的代码就这三行,dispatchListeners
是事件回调函数,dispatchInstances
是对应的组件,将这些参数传入executeDispatch
后:
function executeDispatch(event, simulated, listener, inst) {
var type = event.type || 'unknown-event';
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}
而invokeGuardedCallback
就相当简单了:
function invokeGuardedCallback(name, func, a) {
func(a);
}
上面的func(a)
其实就是listener(event)
,再往上追溯,就是dispatchListeners(dispatchInstances)
,这也就说明为什么我们的React事件回调函数可以拿到原生的事件了。
4.总结
React事件系统为了兼容各种版本的浏览器而做了大量工作,我们不必钻牛角尖去研究这些是如何实现的,与原生事件不同的点,只在于React对事件进行统一而不是分散的存储与管理,捕获事件后内部生成合成事件提高浏览器的兼容度,执行回调函数后再进行销毁释放内存,从而大大提高网页的响应性能。
转载自:https://juejin.cn/post/6844903538762448910