React合成事件是指将原生事件合成一个React事件,之所以要封装自己的一套事件机制,目的是为了实现全浏览器的一致性,抹平不同浏览器之间的差异性。比如原生onclick事件对应React中的onClick合成事件。我们先来看一下React事件和原生事件在使用上的区别:

  1. const handleClick = (e) => {e.preventDefault();}
  2. // 原生事件
  3. <div onclick="handleClick()"></div>
  4. // React合成事件
  5. <div onClick={HandleCilck}></div>

从中可以看到,React合成事件驼峰的命名方式,而原生事件使用全小写的方式;另外,React事件处理函数使用事件对象形式,原生事件使用字符串的形式。UI Events从中可以看到,React合成事件使用驼峰的命名方式,而原生事件使用全小写的方式;另外,React事件处理函数使用事件对象形式,原生事件使用字符串的形式。

事件流

我们已经知道onClick事件是一个合成事件,那合成事件是如何跟原生事件产生关联的呢?首先我们来复习一下事件流原理:

React合成事件系统 - 图1

图片引自:Event flow

如上图所示,所谓事件流包括三个阶段:事件捕获、目标阶段和事件冒泡。事件捕获是从外到里,对应图中的红色箭头标注部分window -> document -> html … -> target,目标阶段是事件真正发生并处理的阶段,事件冒泡是从里到外,对应图中的target -> … -> html -> document -> window。 React合成事件的工作原理大致可以分为两个阶段:

  1. 事件绑定
  2. 事件触发

在React17之前,React是把事件委托在document上的,React17及以后版本不再把事件委托在document上,而是委托在挂载的容器上了,本文以16.x版本的React为例来探寻React的合成事件。当真实的dom触发事件时,此时构造React合成事件对象,按照冒泡或者捕获的路径去收集真正的事件处理函数,在此过程中会先处理原生事件,然后当冒泡到document对象后,再处理React事件。 举个栗子:

  1. import React from 'react';
  2. import './App.less';
  3. class Test extends React.Component {
  4. parentRef: React.RefObject<any>;
  5. childRef: React.RefObject<any>;
  6. constructor(props) {
  7. super(props);
  8. this.parentRef = React.createRef();
  9. this.childRef = React.createRef();
  10. }
  11. componentDidMount() {
  12. document.addEventListener(
  13. 'click',
  14. () => {
  15. console.log(`document原生事件捕获`);
  16. },
  17. true,
  18. );
  19. document.addEventListener('click', () => {
  20. console.log(`document原生事件冒泡`);
  21. });
  22. this.parentRef.current.addEventListener(
  23. 'click',
  24. () => {
  25. console.log(`父元素原生事件捕获`);
  26. },
  27. true,
  28. );
  29. this.parentRef.current.addEventListener('click', () => {
  30. console.log(`父元素原生事件冒泡`);
  31. });
  32. this.childRef.current.addEventListener(
  33. 'click',
  34. () => {
  35. console.log(`子元素原生事件捕获`);
  36. },
  37. true,
  38. );
  39. this.childRef.current.addEventListener('click', () => {
  40. console.log(`子元素原生事件冒泡`);
  41. });
  42. }
  43. handleParentBubble = () => {
  44. console.log(`父元素React事件冒泡`);
  45. };
  46. handleChildBubble = () => {
  47. console.log(`子元素React事件冒泡`);
  48. };
  49. handleParentCapture = () => {
  50. console.log(`父元素React事件捕获`);
  51. };
  52. handleChileCapture = () => {
  53. console.log(`子元素React事件捕获`);
  54. };
  55. render() {
  56. return (
  57. <div
  58. ref={this.parentRef}
  59. onClick={this.handleParentBubble}
  60. onClickCapture={this.handleParentCapture}
  61. >
  62. <div
  63. ref={this.childRef}
  64. onClick={this.handleChildBubble}
  65. onClickCapture={this.handleChileCapture}
  66. >
  67. 事件处理测试
  68. </div>
  69. </div>
  70. );
  71. }
  72. }
  73. export default Test;

上面案例打印的结果为:

React合成事件系统 - 图2

注:React17中上述案例的执行会有所区别,会先执行所有捕获事件后,再执行所有冒泡事件。

事件绑定

通过上述案例,我们知道了React合成事件和原生事件执行的过程,两者其实是通过一个叫事件插件(EventPlugin)的模块产生关联的,每个插件只处理对应的合成事件,比如onClick事件对应SimpleEventPlugin插件,这样React在一开始会把这些插件加载进来,通过插件初始化一些全局对象,比如其中有一个对象是registrationNameDependencies,它定义了合成事件与原生事件的对应关系如下:

  1. {
  2. onClick: ['click'],
  3. onClickCapture: ['click'],
  4. onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
  5. ...
  6. }

registrationNameModule对象指定了React事件到对应插件plugin的映射:

  1. {
  2. onClick: SimpleEventPlugin,
  3. onClickCapture: SimpleEventPlugin,
  4. onChange: ChangeEventPlugin,
  5. ...
  6. }

plugins对象就是上述插件的列表。在某个节点渲染过程中,合成事件比如onClick是作为它的prop的,如果判断该prop为事件类型,根据合成事件类型找到对应依赖的原生事件注册绑定到顶层document上,dispatchEvent为统一的事件处理函数。

事件触发

当任意事件触发都会执行dispatchEvent函数,比如上述事例中,当用户点击Child的div时会遍历这个元素的所有父元素,依次对每一级元素进行事件的收集处理,构造合成事件对象(SyntheticEvent—也就是通常我们说的React中自定义函数的默认参数event,原生的事件对象对应它的一个属性),然后由此形成了一条「链」,这条链会将合成事件依次存入eventQueue中,而后会遍历eventQueue模拟一遍捕获和冒泡阶段,然后通过runEventsInBatch方法依次触发调用每一项的监听事件,在此过程中会根据事件类型判断属于冒泡阶段还是捕获阶段触发,比如onClick是在冒泡阶段触发,onClickCapture是在捕获阶段触发,在事件处理完成后进行释放。 SyntheticEvent对象属性如下:

  1. boolean bubbles
  2. boolean cancelable
  3. DOMEventTarget currentTarget
  4. boolean defaultPrevented
  5. number eventPhase
  6. boolean isTrusted
  7. DOMEvent nativeEvent // 原生事件对象
  8. void preventDefault()
  9. boolean isDefaultPrevented()
  10. void stopPropagation()
  11. boolean isPropagationStopped()
  12. void persist()
  13. DOMEventTarget target
  14. number timeStamp
  15. string type

dispatchEvent伪代码如下:

  1. dispatchEvent = (event) => {
  2. const path = []; // 合成事件链
  3. let current = event.target; // 触发事件源
  4. while (current) {
  5. path.push(current);
  6. current = current.parentNode; // 逐级往上进行收集
  7. }
  8. // 模拟捕获和冒泡阶段
  9. // path = [target, div, body, html, ...]
  10. for (let i = path.length - 1; i >= 0; i--) {
  11. const targetHandler = path[i].onClickCapture;
  12. targetHandler && targetHandler();
  13. }
  14. for (let i = 0; i < path.length; i++) {
  15. const targetHandler = path[i].onClick;
  16. targetHandler && targetHandler();
  17. }
  18. };

总结

由于事件对象可能会频繁创建和回收在React16.x中,合成事件SyntheticEvent采用了事件池,合成事件会被放进事件池中统一管理,这样能够减少内存开销。React通过合成事件,模拟捕获和冒泡阶段,从而达到不同浏览器兼容的目的。另外,React不建议将原生事件和合成事件一起使用,这样很容易造成使用混乱。

最后

搜索公众号Eval Studio,关注获取更多动态

本文转自 https://zhuanlan.zhihu.com/p/395357493,如有侵权,请联系删除。

image.png!image.png

image.png
萧晨课件-事件系统