React合成事件是指将原生事件合成一个React事件,之所以要封装自己的一套事件机制,目的是为了实现全浏览器的一致性,抹平不同浏览器之间的差异性。比如原生onclick事件对应React中的onClick合成事件。我们先来看一下React事件和原生事件在使用上的区别:
const handleClick = (e) => {e.preventDefault();}// 原生事件<div onclick="handleClick()"></div>// React合成事件<div onClick={HandleCilck}></div>
从中可以看到,React合成事件驼峰的命名方式,而原生事件使用全小写的方式;另外,React事件处理函数使用事件对象形式,原生事件使用字符串的形式。UI Events从中可以看到,React合成事件使用驼峰的命名方式,而原生事件使用全小写的方式;另外,React事件处理函数使用事件对象形式,原生事件使用字符串的形式。
事件流
我们已经知道onClick事件是一个合成事件,那合成事件是如何跟原生事件产生关联的呢?首先我们来复习一下事件流原理:

图片引自:Event flow
如上图所示,所谓事件流包括三个阶段:事件捕获、目标阶段和事件冒泡。事件捕获是从外到里,对应图中的红色箭头标注部分window -> document -> html … -> target,目标阶段是事件真正发生并处理的阶段,事件冒泡是从里到外,对应图中的target -> … -> html -> document -> window。 React合成事件的工作原理大致可以分为两个阶段:
- 事件绑定
- 事件触发
在React17之前,React是把事件委托在document上的,React17及以后版本不再把事件委托在document上,而是委托在挂载的容器上了,本文以16.x版本的React为例来探寻React的合成事件。当真实的dom触发事件时,此时构造React合成事件对象,按照冒泡或者捕获的路径去收集真正的事件处理函数,在此过程中会先处理原生事件,然后当冒泡到document对象后,再处理React事件。 举个栗子:
import React from 'react';import './App.less';class Test extends React.Component {parentRef: React.RefObject<any>;childRef: React.RefObject<any>;constructor(props) {super(props);this.parentRef = React.createRef();this.childRef = React.createRef();}componentDidMount() {document.addEventListener('click',() => {console.log(`document原生事件捕获`);},true,);document.addEventListener('click', () => {console.log(`document原生事件冒泡`);});this.parentRef.current.addEventListener('click',() => {console.log(`父元素原生事件捕获`);},true,);this.parentRef.current.addEventListener('click', () => {console.log(`父元素原生事件冒泡`);});this.childRef.current.addEventListener('click',() => {console.log(`子元素原生事件捕获`);},true,);this.childRef.current.addEventListener('click', () => {console.log(`子元素原生事件冒泡`);});}handleParentBubble = () => {console.log(`父元素React事件冒泡`);};handleChildBubble = () => {console.log(`子元素React事件冒泡`);};handleParentCapture = () => {console.log(`父元素React事件捕获`);};handleChileCapture = () => {console.log(`子元素React事件捕获`);};render() {return (<divref={this.parentRef}onClick={this.handleParentBubble}onClickCapture={this.handleParentCapture}><divref={this.childRef}onClick={this.handleChildBubble}onClickCapture={this.handleChileCapture}>事件处理测试</div></div>);}}export default Test;
上面案例打印的结果为:

注:React17中上述案例的执行会有所区别,会先执行所有捕获事件后,再执行所有冒泡事件。
事件绑定
通过上述案例,我们知道了React合成事件和原生事件执行的过程,两者其实是通过一个叫事件插件(EventPlugin)的模块产生关联的,每个插件只处理对应的合成事件,比如onClick事件对应SimpleEventPlugin插件,这样React在一开始会把这些插件加载进来,通过插件初始化一些全局对象,比如其中有一个对象是registrationNameDependencies,它定义了合成事件与原生事件的对应关系如下:
{onClick: ['click'],onClickCapture: ['click'],onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],...}
registrationNameModule对象指定了React事件到对应插件plugin的映射:
{onClick: SimpleEventPlugin,onClickCapture: SimpleEventPlugin,onChange: ChangeEventPlugin,...}
plugins对象就是上述插件的列表。在某个节点渲染过程中,合成事件比如onClick是作为它的prop的,如果判断该prop为事件类型,根据合成事件类型找到对应依赖的原生事件注册绑定到顶层document上,dispatchEvent为统一的事件处理函数。
事件触发
当任意事件触发都会执行dispatchEvent函数,比如上述事例中,当用户点击Child的div时会遍历这个元素的所有父元素,依次对每一级元素进行事件的收集处理,构造合成事件对象(SyntheticEvent—也就是通常我们说的React中自定义函数的默认参数event,原生的事件对象对应它的一个属性),然后由此形成了一条「链」,这条链会将合成事件依次存入eventQueue中,而后会遍历eventQueue模拟一遍捕获和冒泡阶段,然后通过runEventsInBatch方法依次触发调用每一项的监听事件,在此过程中会根据事件类型判断属于冒泡阶段还是捕获阶段触发,比如onClick是在冒泡阶段触发,onClickCapture是在捕获阶段触发,在事件处理完成后进行释放。 SyntheticEvent对象属性如下:
boolean bubblesboolean cancelableDOMEventTarget currentTargetboolean defaultPreventednumber eventPhaseboolean isTrustedDOMEvent nativeEvent // 原生事件对象void preventDefault()boolean isDefaultPrevented()void stopPropagation()boolean isPropagationStopped()void persist()DOMEventTarget targetnumber timeStampstring type
dispatchEvent伪代码如下:
dispatchEvent = (event) => {const path = []; // 合成事件链let current = event.target; // 触发事件源while (current) {path.push(current);current = current.parentNode; // 逐级往上进行收集}// 模拟捕获和冒泡阶段// path = [target, div, body, html, ...]for (let i = path.length - 1; i >= 0; i--) {const targetHandler = path[i].onClickCapture;targetHandler && targetHandler();}for (let i = 0; i < path.length; i++) {const targetHandler = path[i].onClick;targetHandler && targetHandler();}};
总结
由于事件对象可能会频繁创建和回收在React16.x中,合成事件SyntheticEvent采用了事件池,合成事件会被放进事件池中统一管理,这样能够减少内存开销。React通过合成事件,模拟捕获和冒泡阶段,从而达到不同浏览器兼容的目的。另外,React不建议将原生事件和合成事件一起使用,这样很容易造成使用混乱。
最后
搜索公众号Eval Studio,关注获取更多动态
本文转自 https://zhuanlan.zhihu.com/p/395357493,如有侵权,请联系删除。
!
