事件绑定

事件的绑定是在组件的挂载过程中完成的,具体来说,是在 completeWork 中完成的。关于 completeWork,我们已经在第 15 讲中学习过它的工作原理,这里需要你回忆起来的是 completeWork 中的以下三个动作:
completeWork 内部有三个关键动作:创建 DOM 节点(createInstance)、将 DOM 节点插入到 DOM 树中(appendAllChildren)、为 DOM 节点设置属性(finalizeInitialChildren)。

  • 原生事件: 在 componentDidMount生命周期里边进行addEventListener绑定的事件
  • 合成事件: 通过 JSX 方式绑定的事件,比如 onClick={() => this.handle()}

其中“为 DOM 节点设置属性”这个环节,会遍历 FiberNode 的 props key。当遍历到事件相关的 props 时,就会触发事件的注册链路。整个过程涉及的函数调用栈如下图所示:

事件触发

事件触发的本质是对 dispatchEvent 函数的调用。

  1. 循环收集符合条件的父节点,存进 path 数组中
  2. 模拟事件在捕获阶段的传播顺序,收集捕获阶段相关的节点实例与回调函数。path 数组中子节点在前,祖先节点在后。从后往前遍历 path 数组,模拟事件的捕获顺序,收集事件在捕获阶段对应的回调与实例。
  3. 模拟事件在冒泡阶段的传播顺序,收集冒泡阶段相关的节点实例与回调函数。从后往前遍历 path 数组,模拟事件的冒泡顺序,收集事件在捕获阶段对应的回调与实例。

简单的讲挂载的时候,通过listenerBank把事件存起来了,触发的时候document进行dispatchEvent,找到触发事件的最深的一个节点,向上遍历拿到所有的callback放在eventQueue. 根据事件类型构建event对象,遍历执行eventQueue

事件工作流

  • 事件捕获
  • 事件目标
  • 事件冒泡
  • 事件委托
  • 先绑定再执行

image.png

事件差异

image.png

合成事件

React 实现了一个合成事件层,就是这个事件层,把 IE 和 W3C 标准之间的兼容问题给消除了。

  • React 把事件委托到document 对象上
  • 当真实dom元素触发事件,先处理原生事件,然后冒泡到document 对象上,再处理React 事件
  • React 事件绑定的时刻是在reconciliation 阶段,会在原生事件绑定前执行
  • 目的和优势:
  • 进行浏览器兼容,React 采用的是顶层事件代理机制,能保证冒泡的一致性
  • 事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或者释放事件对象(React17 中被废弃)

    React17 之前(绑定到document上)

    使用

    1. import React from 'react';
    2. import ReactDOM from 'react-dom';
    3. import App from './App';
    4. class Event extends React.Component{
    5. parentRef = React.createRef()
    6. childRef = React.createRef()
    7. componentDidMount(){
    8. this.parentRef.current.addEventListener('click', ()=>{
    9. console.log('父元素原生事件捕获')
    10. }, true)
    11. this.parentRef.current.addEventListener('click', ()=>{
    12. console.log('父元素原生事件冒泡')
    13. })
    14. this.childRef.current.addEventListener('click', ()=>{
    15. console.log('子元素原生事件捕获')
    16. }, true)
    17. this.childRef.current.addEventListener('click', ()=>{
    18. console.log('子元素原生事件冒泡')
    19. })
    20. document.addEventListener('click', ()=>{
    21. console.log('document 捕获')
    22. }, true)
    23. // React 会执行一个document.addEventListener('click', dispatchEvent)
    24. // 这个注册是在React注册之后注册的,所以后执行
    25. document.addEventListener('click', ()=>{
    26. console.log('document 冒泡')
    27. })
    28. }
    29. parentBubble = () =>{
    30. console.log('父元素React事件冒泡')
    31. }
    32. childBubble = () =>{
    33. console.log('子元素React事件冒泡')
    34. }
    35. parentCapture = () =>{
    36. console.log('父元素React事件捕获')
    37. }
    38. childCapture = () =>{
    39. console.log('子元素React事件捕获')
    40. }
    41. render() {
    42. return (
    43. <div ref={this.parentRef} onClick={this.parentBubble} onClickCapture={this.parentCapture}>
    44. <p ref={this.childRef} onClick={this.childBubble} onClickCapture={this.childCapture}></p>
    45. </div>
    46. )
    47. }
    48. }
    49. ReactDOM.render(<App/>, document.getElementById('root'))
    image.png

    实现

    ```javascript <!DOCTYPE html>

  1. react 16 中,如果同时注册了原生和react的事件冒泡和事件捕获,执行就会混乱,一会儿冒泡一会儿捕获,为了解决这个问题 react 17 分开注册。
  2. <a name="A22iq"></a>
  3. ## React17 之后(绑定到根元素上)
  4. 事件委托的对象不再是document了,而是挂载容器。这样的话可以让一个页面上使用多个react版本共存。<br />useCapture true 将事件捕获和事件冒泡分开挂载。
  5. <a name="HdVmV"></a>
  6. ### 实现
  7. ```javascript
  8. <!DOCTYPE html>
  9. <html lang="en">
  10. <head>
  11. <meta charset="UTF-8">
  12. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  13. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  14. <title>Document</title>
  15. </head>
  16. <body>
  17. <div id='root'>
  18. <div id='parent'>
  19. <button id='parent'></button>
  20. <button id='child'></button>
  21. </div>
  22. </div>
  23. <script>
  24. let root = document.getElementById('root')
  25. let parent = document.getElementById('parent')
  26. let child = document.getElementById('child')
  27. // 注册react事件委托
  28. root.addEventListener('click', dispatchEvent(event, true), true); // 捕获
  29. root.addEventListener('click', dispatchEvent(event, false));
  30. // useCapture 为true 表示事件捕获
  31. function dispatchEvent(event, useCapture) {
  32. let paths = []; // [child, parent, body]
  33. let current = event.target;
  34. //循环收集符合条件的父节点,存进 path 数组中
  35. while (current) {
  36. paths.push(current)
  37. current = current.parentNode;
  38. }
  39. if (useCapture) {
  40. // 模拟捕获 从外往内
  41. for (let i = paths.length - 1; i >= 0; i--) {
  42. let handler = paths[i].onClickCapture
  43. handler && handler()
  44. }
  45. } else {
  46. // 模拟冒泡 从内往外
  47. for (let i = 0; i < paths.length; i++) {
  48. let handler = paths[i].onClick
  49. handler && handler()
  50. }
  51. }
  52. }
  53. parent.addEventListener('click', () => {
  54. console.log('父元素原生事件捕获')
  55. }, true)
  56. parent.addEventListener('click', () => {
  57. console.log('父元素原生事件冒泡')
  58. })
  59. child.addEventListener('click', () => {
  60. console.log('子元素原生事件捕获')
  61. }, true)
  62. child.addEventListener('click', () => {
  63. console.log('子元素原生事件冒泡')
  64. })
  65. parent.onClick = function () {
  66. console.log('父元素React事件冒泡')
  67. }
  68. parent.onClickCapture = function () {
  69. console.log('父元素React事件捕获')
  70. }
  71. child.onClick = function () {
  72. console.log('子元素React事件冒泡')
  73. }
  74. child.onClickCapture = function () {
  75. console.log('子元素React事件捕获')
  76. }
  77. </script>
  78. </body>
  79. </html>

image.png

捕获先处理 委托在 root上的捕获事件,再处理原生事件捕获,然后处理原生冒泡,冒泡到 root上处理委托到root上的冒泡。

事件系统变更

image.png
示例
点击button显示模态框,点击窗口关闭。
image.png
这个点击button 并不会显示,因为在 react 16 里,事件是被绑定到了document上,所以相当于在document上触发了两次事件,第一次事件会让show变为true,第二次事件马上会让show变为 false,所以就不会显示。要解决这个问题就要使用event.stopImmediatePropagation,会将绑定在同一个元素上的其他监听函数不再执行。
image.png
image.png