某次被问到 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