某次被问到 React事件机制的问题,关于这一块我确实不怎么清楚,因为平时大部分工作都是用 Vue,对于 React的熟悉程度只限于会用,具体实现逻辑还真没专门学习过,但是总不能就说自己不清楚吧,好在我了解 Vue的事件机制,于是就把 Vue的事件机制说了一遍,最后再来一句“我觉得 React应该和 Vue的差不多”
后来我想了下应该没那么简单,于是网上搜了下相关文章,发现果然是被我想得太简单了,Vue通过编译模板,解析出事件指令,将事件和事件回调附加到 vnode tree上,在 patch过程中的创建阶段和更新阶段都会对这个 vnode tree进行处理,拿到每个 vnode上附加的事件信息,就可以调用原生 DOM API对相应事件进行注册或移除,流程还是比较清晰的,而React则是单独实现了一套事件机制
本文以
React v16.5.2为基础进行源码分析
基本流程
在 react源码的 react-dom/src/events/ReactBrowserEventEmitter.js文件的开头,有这么一大段注释:
/**
* Summary of `ReactBrowserEventEmitter` event handling:
*
* - Top-level delegation is used to ......
* ......
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|EventPluginHub| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
* React Core . General Purpose Event Plugin System
*/
这段注释第一段文本内容被我省略掉了,其主要是在大概描述 React的事件机制,也就是这个文件中的代码要做的一些事情,大概意思就是说事件委托是很常用的一种浏览器事件优化策略,于是 React就接管了这件事情,并且还贴心地消除了浏览器间的差异,赋予开发者跨浏览器的开发体验,主要是使用 EventPluginHub这个东西来负责调度事件的存储,合成事件并以对象池的方式实现创建和销毁,至于下面的结构图形,则是对事件机制的一个图形化描述
根据这段注释,大概可以提炼出以下几点内容:
React事件使用了事件委托的机制,一般事件委托的作用都是为了减少页面的注册事件数量,减少内存开销,优化浏览器性能,React这么做也是有这么一个目的,除此之外,也是为了能够更好的管理事件,实际上,React中所有的事件最后都是被委托到了document这个顶级DOM上- 既然所有的事件都被委托到了
document上,那么肯定有一套管理机制,所有的事件都是以一种先进先出的队列方式进行触发与回调 - 既然都已经接管事件了,那么不对事件做些额外的事情未免有些浪费,于是
React中就存在了自己的 合成事件(SyntheticEvent),合成事件由对应的EventPlugin负责合成,不同类型的事件由不同的plugin合成,例如SimpleEvent Plugin、TapEvent Plugin等 - 为了进一步提升事件的性能,使用了
EventPluginHub这个东西来负责合成事件对象的创建和销毁
下文均以下述这段代码为示例进行分析:
export default class MyBox extends React.Component {
clickHandler(e) {
console.log('click callback', e)
}
render() {
return (
<div className="box" onClick={
this.clickHandler}>文本内容</div>
)
}
}
事件注册
只看相关主体流程,其他诸如 vnode的创建等前提流程就不管了,从setInitialDOMProperties这个方法开始看起,这个方法主要用于遍历 ReactNode的 props对象,给最后将要真正渲染的真实 DOM对象设置一系列的属性,例如 style、class、autoFocus,也包括innerHTML、event的处理等,示例中 .box元素的 props对象结构如下:

这个方法中有个 case,就是专门用于处理事件的:
// react-dom/src/client/ReactDOMComponent.js
else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (true && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
// 处理事件类型的 props
ensureListeningTo(rootContainerElement, propKey);
}
}
其中的 registrationNameModules这个变量,里面存在一大堆的属性,都是与 React的事件相关:
例子中的 onClick这个 props显然符合,所以可以执行 ensureListeningTo这个方法:
// react-dom/src/client/ReactDOMComponent.js
function ensureListeningTo(rootContainerElement, registrationName) {
var isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
var doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument;
listenTo(registrationName, doc);
}
这个方法中,首先判断了 rootContainerElement是不是一个 document或者 Fragment(文档片段节点),示例中传过来的是 .box这个 div,显然不是,所以 doc这个变量就被赋值为 rootContainerElement.ownerDocument,这个东西其实就是 .box所在的 document元素,把这个document传到下面的 listenTo里了,事件委托也就是在这里做的,所有的事件最终都会被委托到 document 或者 fragment上去,大部分情况下都是 document,然后这个 registrationName就是事件名称 onClick
接着开始执行 listenTo方法,这个方法其实就是注册事件的入口了,方法里面有这么一句:
// react-dom/src/events/ReactBrowserEventEmitter.js
var dependencies = registrationNameDependencies[registrationName]

本文深入探讨React的事件机制,从基本流程、事件注册、事件分发到事件执行,揭示React如何通过合成事件实现跨浏览器兼容并优化性能。通过对源码的分析,阐述了事件委托、事件池以及如何构造和复用合成事件对象,同时讲解了事件的分发和执行过程。
最低0.47元/天 解锁文章
753

被折叠的 条评论
为什么被折叠?



