为什么要React实现自己的事件机制
● 将事件代理到根节点,减少事件监听器创建 节省内存
● 磨平浏览器的差异(阻止事件传播只需:event.stopPropagation)
● 只要在对应节点编写onClick、onClickCapture 可完成节点冒泡、捕获阶段监听,统一写法
事件分类
react对事件进行分类,不同事件通过不同类型的事件插件处理
● 简单事件 SimpleEventPlugin( onClick、onKeyDown 、…)
● 输入前事件 BeforeInputEventPlugin ( onBeforeInput 、 … )
● 表单修改事件 ChangeEventPlugin ( onChange 、…)
● 鼠标进出事件 EnterLeaveEventPlugin ( onMouseEnter… )
● 选择事件 SelectEventPlugin (onSelect)
分类不代表依赖的原生事件没有交集, 简单事件的onKeyDown、输入前事件onCompositionStart依赖、表单修改事件onChange 都依赖原生事件keydown。react对一些事件也进行了包装如: onChange事件,兼容不同表单的修改事件 依赖了原生事件change, click, focusin, input,keydown,keyup,selectionchange
事件收集
react对事件做代理,需要知道浏览器知道的事件,这些事件都硬编码在各个事件插件中。对所需代理的原生事件,会以原生事件名字符串形式存储在allNativeEvents中并在registrationNameDependencies中储存react事件名到依赖的原生事件名数的映射。
事件的收集是通过各个事件插件各自收集注册。页面加载会执行各个插件的registerEvents,将所有依赖的原生事件都注册到allNativeEvents中,并在registrationNameDependencies中存储映射关系
对原生事件不支持冒泡的事件,硬编码的形式存储在nonDelegatedEvents集合中; 不支持冒泡阶段事件和支持冒泡阶段事件处理方式不一样
● 不支持冒泡事件 -> 非代理事件
● 支持冒泡阶段事件 -> 代理事件
// react代码加载时就会执行各个事件的插件
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
// 执行完成后allNativeEvents集合就有所需代理的原生事件
allNativeEvents = [click, input, ...];
// nonDelegatedEvents 不支持冒泡阶段原生事件集合
nonDelegatedEvents = [...];
// 保存react事件和其依赖的事件的映射
registrationNameDependencies = {
onClick: [ 'click' ],
onChange: ['change','click','focusin','focusout','input','keydown','keyup','selectionchange'],
...
}
事件代理
-
代理事件
在ReactDOM.render(element, container)时,将事件委托代理到根;在ReactDom.render实现中,创建fiberRoot后, 开始构建fiber树前,调用 listenToAllSupportedEvents进行事件绑定委托。
-
非代理事件(对不可冒泡的事件都进行了冒泡模拟)
事件代理在Dom实例创建阶段(render阶段的completeWork阶段),通过调用finalizeInitialChildren为实例dom设置属性时 判断dom节点类型添加响应的冒泡阶段监听器
-
合成事件
react对原生事件跨浏览器的包装器,兼容了所有浏览器,拥有和浏览器原生事件相同的接口;使用小驼峰命名(如click 冒泡: onClick, 捕获: onClickCapture)
事件触发
当页面上触发了特定的事件时,如点击事件 click,就会触发绑定在根元素上的事件回调函数,也就是之前绑定了参数的dispatchEvent,而dispatchEvent在内部最终会调用dispatchEventsForPlugins
● 根据原生事件名,得到对应的react事件名
● 判断需要使用的合成事件构造函数
● 根据绑定的事件标记得出事件是否捕获阶段
● 从触发事件的 DOM 实例对应的 fiber 节点开始,向上遍历 fiber 树,判断遍历到的 fiber 是否宿主类型 fiber 节点,是的话判断在其 props 上是否存在 React 事件名同名属性,如果存在,则 push 到数组中,遍历结束即可收集由叶子节点到根节点的回调函数
● 如果收集的回调数值不为空,则实例化对应的合成事件,并与收集的回调函数一同收集到dispatchQueue中
磨平浏览器差异
● 通过事件normalize, 在不同的浏览器中拥有一致属性
● 声明了各种事件接口,磨平浏览器中的差异
16 -> 17
● 由document 改为react根容器
● 存在多个react应用, 都会在顶层document注册处理器,其中一个调用了e.stopPropagation(), 无法阻止事件冒泡到外部树,真实事件已传播
减少多个react应用并存产生的问题;事件系统更贴近现在浏览器表现
事件代理阶段
16: 事件委托在冒泡阶段,当事件冒泡到document后触发绑定的回调,在回调中重新模拟一次 捕获-冒泡行为. e.stopPropagation()无法阻止原生的原生冒泡或捕获,因为已经执行完成。
17: 事件委托分别在捕获和冒泡阶段
- 根容器接收到捕获事件时,先触发一次react捕获阶段,然后在执行原生的捕获传播,在捕获调用e.stopPropagation()能阻止原生事件传播
- 接收冒泡事件时,会触发react事件的冒泡阶段,原生事件的冒泡已经传播到了根,在冒泡阶段调用e.stopPropagation()不能阻止原生事件向根传播,但是可以阻止根容器向页面顶层传播。