前言
近期了解了 React 的合成事件,通过文档的形式沉淀。
前置知识
在了解 React 的事件机制之前,我们先回顾一些 HTML 相关的事件知识,以便后面更好的 React 的事件机制。
event.target 与 event.currentTarget 的区别
event.target 是触发事件的元素,这儿常用于事件代理
event.currentTarget 是绑定事件的元素
验证地址: 传送门
对于 React 来说可以通过 React.nativeTarget 判断 React 代理了哪个事件
例如我们给 input 输入框添加了 onChange 事件,通过 React.nativeTarget 我们可以发现,实际上 React 是代理了 Input 事件
验证地址: 传送门
事件捕获与事件冒泡
当点击后某个目标元素时,会按如下顺序触发: 事件捕获 → 目标元素 → 事件冒泡。
验证地址: 传送门
stopPropagation VS stopImmediatePropagation
stopPropagation 会阻止冒泡
stopImmediatePropagation 不仅会阻止冒泡,还会阻止监听对目标元素的监听事件
验证地址: 传送门
事件代理
事件代理是通过事件冒泡,父节点捕获到事件后,然后进行处理。其优点在于减少内存的开销和动态绑定事件
验证地址: 传送门
事件机制初识
为了兼容不同浏览器以及管理事件,React 自身实现了一套自己的事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等,虽然和原生的是两码事,但也是基于浏览器的事件机制下完成的。
我们都知道 react 的所有事件并没有绑定到具体的dom节点上而是绑定在了根节点上(解决内存),然后由统一的事件处理程序来处理,同时也是基于浏览器的事件机制(如冒泡),所有节点的事件都会在根节点上触发。
在 JSX 绑定的事件,最终会转化成什么?
了解到 React 的 JSX 最终通过 babel 进行编译成 React.createElement 函数。
因此我们可以在 babel 的 在线地址 添加插件 @babel/plugin-transform-react-jsx 在线查看在 JSX 绑定的事件,最终会转化为什么。
从上图上可以看出,最终事件会被传入 React.createElement 的 props 参数中。
React 绑定的事件真的在元素上么?
我们写了俩个组件一个是原生组件,一个是 React 组件,分别绑定了 input 事件, 通过查看事件监听器,通过原生事件绑定的事件会挂载在目标元素上,但通过 React 事件绑定的事件并不会挂载目标元素上,而是挂载在了 Root 上。
Vue中的 Event 对象是原生的,而 React 中的 Event 是进行封装的,可以通过 React.nativeEvent 获取原生事件。
验证地址1: 传送门
验证地址2: 传送门
正文开始
事件分类
React 对所有的事件进行了分类,具体通过各个类型的事件处理插件分别处理:
SimpleEventPlugin简单事件,代表事件onClick
BeforeInputEventPlugin输入前事件,代表事件onBeforeInput
ChangeEventPlugin表单修改事件,代表事件onChange
EnterLeaveEnventPlugin鼠标进出事件,代表事件onMouseEnter
-
SelectEventPlugin选择事件,代表事件onSelect
从源代码中我们可以发现 React 的 onClick 事件比较简单,只依赖了 click 事件,而 onChange 事件依赖了 ['change', 'click', 'focusin', 'focusout', 'input', 'keydown', 'keyup', 'selectionchange']
事件收集
由上文可知,React 会对所有事件进行事件代理,因此需要事先知道浏览器支持的所有事件,这些都是定义好在 React 源代码中的,如下:
在页面加载时,会将所有的事件全部收集到变量 allNativeEvents 中,并维护一个对象 registrationNameDependencies,这个对象存储着 React 事件与其依赖原生事件的关系,如 { onClick: [click, ...]}
// React代码加载时就会执行以下js代码
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
// 上述代码执行完后allNativeEvents集合中就会有cancel、click等80种事件
allNativeEvents = ['cancel','click', ...]
// nonDelegatedEvents有cancel、close等29种事件
nonDelegatedEvents = ['cancel','close',...]
// registrationNameDependencies保存react事件和其依赖的事件的映射
registrationNameDependencies = { onClick: ['click'], onClickCapture: ['click'], onChange: ['change','click','focusin','focusout','input','keydown','keyup','selectionchange'], ... }
事件代理
将事件委托代理到根的操作发生在ReactDOM.render(element, container)时。
在 ReactDOM.render 的实现中,在创建了 fiberRoot 后,在开始构造 fiber 树前,会调用 listenToAllSupportedEvents进行事件的绑定委托。
通过 listenToAllSupportedEvents 我们可以发现,会通过 nonDelegatedEvents 判断是否是只有捕获事件,如果只有捕获事件则不触发冒泡,最终都会通过 listenToNativeEvent 方法挂载事件。
在 listenToNativeEvent 函数中会去创建事件的绑定函数
在 createEventListenerWrapperWithPriority 函数中我们可以发现,不同的事件会返回不同的优先级,大致可以分成三种类型
DiscreteEvent:离散事件。如 click、keydown、focusin 等,这些事件的触发不是连续的,可以快速响应,优先级最高
UserBlockingEvent:用户交互阻塞渲染的事件。如 drag、scroll 等,优先级适中
ContinuousEvent 与 default:连续事件和默认事件。连续事件如 playing、load 等,优先级最低
根据事件名获取到不同优先级的触发事件函数后,将函数绑定到根节点上。
事件触发
因为 dispatchDiscreteEvent、dispatchUserBlockingUpdate 都是基于 dispatchEvent 做的二次封装,因此,我们着重看 dispatchEvent 函数即可。
在 attemptToDispatchEvent 函数中,会获取触发事件的 DOM 元素,根据 DOM 元素获取到 Fiber 节点,然后调用插件系统 dispatchEventForPluginEventSystem,
dispatchEventForPluginEventSystem 会去收集 Fiber 节点上的事件,并派发更新
dispatchEventForPluginEventSystem 会调用 dispatchEventsForPlugins 方法
dispatchEventsForPlugins 会根据事件名生成对应的合成事件,并从当前 Fiber 节点出发,分别在捕获阶段和冒泡阶段收集节点上所有监听该事件的 listener,往事件派发队列添加事件
processDispatchQueue 会遍历收集到的事件,根据是冒泡还是捕获进行事件的调用。
React16 与 React17 事件对比
React16 时,合成事件放在了 document 的冒泡上,在 document 的冒泡事件之前上进行合成事件的捕获和冒泡事件的触发。
React17 时,为了兼容多个 React 版本将合成事件的捕获放在了根节点的捕获上,合成事件的冒泡放在了根节点的冒泡事件上。
React16: document 捕获 -> 原生事件 -> 合成事件 -> document 冒泡 (原因是事件委托在 document 的冒泡事件上)
React17: document 捕获 -> 合成捕获 -> 原生捕获 -> 原生冒泡 -> 合成冒泡 -> document 冒泡 (原因是合成事件的委托在 root 容器的捕获和冒泡事件上了测试链接
验证地址:
React16: codesandbox.io/s/react16-e…
React17: codesandbox.io/s/react17-h…
总结
React 事件机制主要是为了统一管理事件和抹平各个浏览器之间的差异而产生的。
React 事件机制主要可以分成三大部分
1 事件收集:React在会对所有的事件进行分类,生成 React 事件对应原生事件的集合。
2 事件绑定:在初始化时,在根节点绑定事件,对于没有冒泡的事件,会在目标节点绑定对应的事件
3 事件触发:目标节点的 Fiber 节点会一直向上收集对应的事件,根据冒泡或者捕获进行触发。