事件绑定
事件的绑定是在组件的挂载过程中完成的,具体来说,是在 completeWork 中完成的。关于 completeWork,我们已经在第 15 讲中学习过它的工作原理,这里需要你回忆起来的是 completeWork 中的以下三个动作:
completeWork 内部有三个关键动作:创建 DOM 节点(createInstance)、将 DOM 节点插入到 DOM 树中(appendAllChildren)、为 DOM 节点设置属性(finalizeInitialChildren)。
- 原生事件: 在 componentDidMount生命周期里边进行addEventListener绑定的事件
 - 合成事件: 通过 JSX 方式绑定的事件,比如 onClick={() => this.handle()}
 
其中“为 DOM 节点设置属性”这个环节,会遍历 FiberNode 的 props key。当遍历到事件相关的 props 时,就会触发事件的注册链路。整个过程涉及的函数调用栈如下图所示:
事件触发
事件触发的本质是对 dispatchEvent 函数的调用。
- 循环收集符合条件的父节点,存进 path 数组中
 - 模拟事件在捕获阶段的传播顺序,收集捕获阶段相关的节点实例与回调函数。path 数组中子节点在前,祖先节点在后。从后往前遍历 path 数组,模拟事件的捕获顺序,收集事件在捕获阶段对应的回调与实例。
 - 模拟事件在冒泡阶段的传播顺序,收集冒泡阶段相关的节点实例与回调函数。从后往前遍历 path 数组,模拟事件的冒泡顺序,收集事件在捕获阶段对应的回调与实例。
 
简单的讲挂载的时候,通过listenerBank把事件存起来了,触发的时候document进行dispatchEvent,找到触发事件的最深的一个节点,向上遍历拿到所有的callback放在eventQueue. 根据事件类型构建event对象,遍历执行eventQueue
事件工作流
- 事件捕获
 - 事件目标
 - 事件冒泡
 - 事件委托
 - 先绑定再执行
 
事件差异
合成事件
React 实现了一个合成事件层,就是这个事件层,把 IE 和 W3C 标准之间的兼容问题给消除了。
- React 把事件委托到document 对象上
 - 当真实dom元素触发事件,先处理原生事件,然后冒泡到document 对象上,再处理React 事件
 - React 事件绑定的时刻是在reconciliation 阶段,会在原生事件绑定前执行
 - 目的和优势:
 - 进行浏览器兼容,React 采用的是顶层事件代理机制,能保证冒泡的一致性
 - 事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或者释放事件对象(React17 中被废弃)
React17 之前(绑定到document上)
使用
import React from 'react';import ReactDOM from 'react-dom';import App from './App';class Event extends React.Component{parentRef = React.createRef()childRef = React.createRef()componentDidMount(){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('子元素原生事件冒泡')})document.addEventListener('click', ()=>{console.log('document 捕获')}, true)// React 会执行一个document.addEventListener('click', dispatchEvent)// 这个注册是在React注册之后注册的,所以后执行document.addEventListener('click', ()=>{console.log('document 冒泡')})}parentBubble = () =>{console.log('父元素React事件冒泡')}childBubble = () =>{console.log('子元素React事件冒泡')}parentCapture = () =>{console.log('父元素React事件捕获')}childCapture = () =>{console.log('子元素React事件捕获')}render() {return (<div ref={this.parentRef} onClick={this.parentBubble} onClickCapture={this.parentCapture}><p ref={this.childRef} onClick={this.childBubble} onClickCapture={this.childCapture}></p></div>)}}ReactDOM.render(<App/>, document.getElementById('root'))
实现
```javascript <!DOCTYPE html> 
react 16 中,如果同时注册了原生和react的事件冒泡和事件捕获,执行就会混乱,一会儿冒泡一会儿捕获,为了解决这个问题 react 17 分开注册。<a name="A22iq"></a>## React17 之后(绑定到根元素上)事件委托的对象不再是document了,而是挂载容器。这样的话可以让一个页面上使用多个react版本共存。<br />useCapture 为true 将事件捕获和事件冒泡分开挂载。<a name="HdVmV"></a>### 实现```javascript<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title></head><body><div id='root'><div id='parent'><button id='parent'></button><button id='child'></button></div></div><script>let root = document.getElementById('root')let parent = document.getElementById('parent')let child = document.getElementById('child')// 注册react事件委托root.addEventListener('click', dispatchEvent(event, true), true); // 捕获root.addEventListener('click', dispatchEvent(event, false));// useCapture 为true 表示事件捕获function dispatchEvent(event, useCapture) {let paths = []; // [child, parent, body]let current = event.target;//循环收集符合条件的父节点,存进 path 数组中while (current) {paths.push(current)current = current.parentNode;}if (useCapture) {// 模拟捕获 从外往内for (let i = paths.length - 1; i >= 0; i--) {let handler = paths[i].onClickCapturehandler && handler()}} else {// 模拟冒泡 从内往外for (let i = 0; i < paths.length; i++) {let handler = paths[i].onClickhandler && handler()}}}parent.addEventListener('click', () => {console.log('父元素原生事件捕获')}, true)parent.addEventListener('click', () => {console.log('父元素原生事件冒泡')})child.addEventListener('click', () => {console.log('子元素原生事件捕获')}, true)child.addEventListener('click', () => {console.log('子元素原生事件冒泡')})parent.onClick = function () {console.log('父元素React事件冒泡')}parent.onClickCapture = function () {console.log('父元素React事件捕获')}child.onClick = function () {console.log('子元素React事件冒泡')}child.onClickCapture = function () {console.log('子元素React事件捕获')}</script></body></html>

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

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

