说到 React 的事件,也算是 React 的一个非常有亮点的优化,它在原生事件体系的基础上,单独实现了一套事件机制。想要了解这个机制,首先的了解下什么是事件委托以及事件委托的好处。
事件委托
假设有如下 html,我们想要在每个 li 上绑定 onClick 事件,最直观的做法当然就是给每个 li 分别添加事件,增加事件回调。这种做法当然没错,但是我们有一种更好的做法,那就是在 ul 上添加有个监听事件,由于事件的冒泡机制,事件就会冒泡到ul上,因为ul上有事件监听,所以事件就会触发。
Event 对象提供了一个属性叫 target,可以返回事件的目标节点,我们成为事件源,也就是说,target 就可以表示为触发当前的事件 dom,我们可以根据 dom 进行判断到底是哪个元素触发了事件,根据不同的元素,执行不同的回调方法。
这就是事件委托。
<
事件委托有以下优点。
- 减少事件注册,节省内存,能够提升整体性能。
- 简化了dom节点更新时,相应事件的更新(用过 jquery 的都知道,动态加入的元素,事件需要重新绑定)。
React 数据结构
React 并不是将 click 事件直接绑定在 dom 上面,而是采用事件冒泡的形式冒泡到 document 上面,这个思路借鉴了事件委托机制。所以,React 中所有的事件最后都是被委托到了 document这个顶级 DOM 上。
这个事件委托是在什么时候完成的呢?我们看如下示例。
<
这就是一个普通的 div,经过 React 的处理后,render 中的内容会变成以下数据结构。
{
React 会根据这个数据结构来构造真实的 dom,如果对之前的内容还有印象,我们在创建 HostComponent,也就是原生节点的时候,会调用 ensureListeningTo(rootContainerElement, propKey) 这个方法。
function
ensureListeningTo 就是用来完成事件委托的,对于本例来说,rootContainerElement 表示项目挂载的根节点 div,registrationName 就是我们绑定事件的名称 onClick。div 的 nodeType 是ELEMENT_NODE,也就是元素节点,那么会通过该元素的 ownerDocument 属性获得文档的 Document 对象,然后将 onClick 事件挂载到 Document 对象上。
了解了这点后,我们先不看 listenTo 方法,回过头来了解下 React 事件的整理流程。
React 事件流程
react 源码中有一个注释很好的总结了 React 的事件流程(在注释中画流程图,很社会)。
react-domsrceventsReactBrowserEventEmitter.js
/**
从这个图可以看出,Dom 事件发生后,React 通过事件委托机制将大部分事件代理至 Document 层,ReactEventListener 就是负责给元素绑定事件的。ReactEventEmitter 暴露接口给 React 组件层用于添加事件订阅(对外暴露了 listenTo 等方法)。EventPluginHub 负责管理和注册各种插件。在事件分发时,调用插件来生成合成事件。 React 事件系统使用了插件机制来管理不同行为的事件。这些插件会处理自己感兴趣的事件类型,并生成合成事件对象。
比如 SimpleEventPlugin 负责处理一些比较通用的事件类型,如blur、focus、click、submit、touchMove、mouseMove、scroll、drag、load。
EnterLeaveEventPlugin 负责处理 mouseEnter/mouseLeave 和 pointerEnter/pointerLeave 这两类事件,单独处理的原因是这两类事件不支持冒泡。
TapEventPlugin 是为了解决移动端IOS 300ms 点击延迟,该插件增加了一个 onTouchTap 事件,这个事件触发后,会忽略300ms 后的 onClick 事件。
这里还需要了解的是,EventPluginHub 中处理的时间其实是合成事件 (SyntheticEvent),React 为什么要定义合成事件这个概念呢,有三点原因:
- 合成事件 (SyntheticEvent) 可以认为是浏览器原生事件跨浏览器的封装,相当于 React 帮我们做了浏览器的兼容性处理。
- React 想通过 SyntheticEvent 实现跨平台事件机制。
- 原生事件升级、改造,比如 React 的 onChange 事件,它为表单元素定义了统一的值变动事件,例如 blur、change、focus、input 等。
React 事件委托
了解了 React 大概的事件体系后,接着看之前的 ensureListeningTo 方法,该方法总调用了 listenTo 方法,这个方法就是 ReactEventEmitter 中的(对应着上面的图看会清晰很多)。
react-domsrceventsReactBrowserEventEmitter.js
export
我们看下 registrationNameDependencies 的数据结构。registrationNameDependencies 比较多,我们看些熟悉的。registrationNameDependencies 存放的是 React 事件名与原生事件名映射的 Map。可以很明显的看到,有些事件是合成事件,比如 onBeforeInput,onChange 等。onClick 事件就是原生的 click。这些值都是插件赋予的。
{
对于依赖的原生事件,scroll blur focus cancel close 方法注册捕获阶段的事件监听器。invalid submit reset 事件不做处理。剩下的事件需要判断是否是媒体触发的,比如 video / audio 的 onplaying 事件,onprogress 事件, onratechange 事件等,这些媒体事件也不需要处理。
React 这么做的原因和事件有关,有些事件是不冒泡的,所以不能在冒泡阶段进行事件委托。具体情况请看我的另一篇文章“JavaScript那些不会冒泡的事件”,大家可以先去了解下这些不会冒泡的事件,回头看 React 的处理流程会比较清晰。
上述代码中处理了个特殊的情况,那就是 form 的 submit invalid,reset 事件,这几个事件没有委托给 Document 对象,如下所示。
case
为什么呢?原因是如果将 submit 事件绑定到 Form 上,该事件的监听方法会触发两次。当提交按钮被点击后,事件开始冒泡,当冒泡到 Form 的上的时候,由于 Form 存在 onSubmit 回调,会执行一次,然后向上冒泡到 Document,还会执行一次。所以 React 对于 Form 事件就不做处理了。
<
React 同样也没有处理媒体事件,和 Form 事件不同的是,媒体事件是没有冒泡过程的,所以只能在捕获阶段进行事件委托。
<
经过测试后发现,媒体事件如果在捕获阶段进行事件委托,该事件的回调方法会被执行两次,和 Form 的表现是一致的,所以媒体事件没有委托。
除了上述的特殊事件,剩下的事件执行 trapBubbledEvent,也就是注册冒泡阶段的事件监听器,click 事件当然也处在这个行列。
trapBubbledEvent 和 trapCapturedEvent 大同小异,本例中触发的 click 事件属于冒泡事件,我们分析 trapBubbledEvent 方法。
react-domsrceventsReactDOMEventListener.js
export
trapBubbledEvent 调用 trapEventForPluginEventSystem 方法
function
React 中的事件分为 3 类。分别是 DiscreteEvent(离散事件),UserBlockingEvent(用户阻塞事件),ContinuousEvent(连续事件)。不同类型的事件代表了不同的优先级。
- DiscreteEvent:click,blur,focus,submit,tuchStart 等,优先级是 0。
- UserBlockingEvent:touchMove,mouseMove,scroll,drag,dragOver 等,这些事件会阻塞用户的交互,优先级是 1。
- ContinuousEvent:load,error,loadStart,abort,animationend 等,优先级是 2,这个优先级最高,不会被打断。
根据优先级的不同,监听函数做了不同的包装,我们先不管这里生成的监听函数和最初的监听方法有什么不同。最终我们会调用 addEventBubbleListener 方法。
addEventBubbleListener 就是 element.addEventListener,为目标添加事件监听函数。
export
到此为止,一个事件 click 事件的监听方法就被添加到 Document 对象上了。小结一下:
- React 借鉴事件委托的方式将大部分事件委托给了 Document 对象。
- React 中的事件分为 3 类。分别是 DiscreteEvent(离散事件),UserBlockingEvent(用户阻塞事件),ContinuousEvent(连续事件)。不同类型的事件代表了不同的优先级。
- 事件委托需要区分捕获和冒泡,有些事件由于没有冒泡过程,只能在捕获阶段进行事件委托。
- 没有进行委托的事件是 Form 事件和 Media 事件,原因是这些事件委托后会触发两次回调函数。
建议先阅读我的另一篇文章JavaScript 中那些不会冒泡的事件,这样会更容易理解 React 对于事件的处理过程
如果您觉得有所收获,就麻烦点个赞吧!