0.前言
2年半前端练习生,第一篇文章记录一下。
初衷:与其闭门造车,不如发出来让大伙批评再学习改进。
1.是什么?
React给予浏览器的事件机制实现了一套自己的事件机制,包括事件注册、事件合成、事件冒泡等等内容。
这套React的事件机制就被称为React的合成事件。
2.做了什么?
要搞清楚React的合成事件做了什么?我们先回顾下js的事件的基础概念。
js的事件分为事件和事件流两个概念。
事件是与浏览器之间的交互,让网页具有互动性,比如说click事件(点击某个元素会产生什么效果)
事件流分为三个阶段,捕获、目标、冒泡阶段(分别从上到下、到目标、再从下到上。并且一般捕获阶段不执行)
ok,那我们从js的原生事件出发,React也要实现这样一套东西,所以他也得有事件、事件流。所以我们可以从这两个概念入手,看看React的合成事件做了什么?
2.1.事件-合成事件对象
合成事件对象这个是对浏览器原生事件对象的一层封装,兼容了主流的浏览器,同时拥有和浏览器原生事件相同的 API,例如 stopPropagation 和 preventDefault。
合成事件存在的目的就是为了消除不同浏览器在事件对象上面的一个差异。
2.2.事件流-模拟实现事件传播机制
利用事件委托的原理,React 会基于 FiberTree 来实现了事件的捕获、目标以及冒泡的过程(就类似于原生 DOM 的事件传递过程)。
并且React不在具体DOM节点绑定事件,而是在document
层统一监听,通过事件冒泡捕获并分发事件。
具体模拟实现:
- 事件委托:
-
- 在根元素绑定事件,当所有子孙元素触发该类事件时最终会委托给根元素来处理(统一由根元素处理,并进行事件派发)
- 事件派发
-
- 寻找触发事件的 DOM 元素,找到对应的 FiberNode(这时候有真实DOM,通过真实DOM的fiber属性拿到FiberNode)
- 收集从当前的 FiberNode 到 HostRootFiber 之间所有注册了该事件的回调函数
- 反向遍历并执行一遍收集的所有的回调函数(模拟捕获阶段的实现)
- 正向遍历并执行一遍收集的所有的回调函数(模拟冒泡阶段的实现)
typescript
// 首先我们通过 addEvent 来给根元素绑定事件,目前是为了使用事件委托
/**
* 该方法用于给根元素绑定事件
* @param {*} container 根元素
* @param {*} type 事件类型
*/
export const addEvent = (container, type) => {
container.addEventListener(type, (e) => {
// 进行事件的派发
dispatchEvent(e, type.toUpperCase());
});
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(jsx);
// 进行根元素的事件绑定,换句话说,就是使用我们自己的事件系统
addEvent(document.getElementById("root"), "click");
scss
// 事件派发
/**
*
* @param {*} e 原生的事件对象
* @param {*} type 事件类型,已经全部转为了大写,比如这里传递过来的是 CLICK
*/
const dispatchEvent = (e, type) => {
// 实例化一个合成事件对象
const se = new SyntheticEvent(e);
// 拿到触发事件的元素
const ele = e.target;
let fiber;
// 通过 DOM 元素找到对应的 FiberNode
for (let prop in ele) {
if (prop.toLocaleLowerCase().includes("fiber")) {
fiber = ele[prop];
}
}
// 找到对应的 fiberNode 之后,接下来我们需要收集路径中该事件类型所对应的所有的回调函数
const paths = collectPaths(type, fiber);
// 模拟捕获的实现
triggerEventFlow(paths, type + "CAPTURE", se);
// 模拟冒泡的实现
// 首先需要判断是否阻止了冒泡,如果没有,那么我们只需要将 paths 进行反向再遍历执行一次即可
if(!se._stopPropagation){
triggerEventFlow(paths.reverse(), type, se);
}
};
3.原生事件和合成事件
3.1执行顺序(区分16和17版本)
执行顺序-React17(事件委托render的根节点-root,而不是document)
(原生document捕获-合成捕获-root捕获-原生捕获-原生冒泡-合成冒泡-root冒泡-document冒泡)
- 原生-document捕获
- 合成-父元素捕获
- 合成-子元素捕获
- 原生-root捕获
- 原生-父元素捕获
- 原生-子元素捕获
- 原生-子元素冒泡
- 原生-父元素冒泡
- 合成-子元素冒泡
- 合成-父元素冒泡
- 原生-root冒泡
- 原生-document冒泡 执行顺序-React16(事件委托到了document)
(原生document捕获-原生捕获-原生冒泡-合成捕获-合成冒泡-原生document冒泡)
- 原生-document捕获
- 原生-父元素捕获
- 原生-当前元素捕获
- 原生-当前元素冒泡
- 原生-父元素冒泡
- 合成-父元素捕获
- 合成-子元素捕获
- 合成-子元素冒泡
- 合成-父元素冒泡
- 原生-document冒泡
3.2.为什么React16到17要更改委托对象
为什么React16事件委托到document,而React17事件委托到root?
微前端场景下,多个独立 React 应用可能共存于同一页面。旧版全局 document
委托会导致事件冲突,而新版每个应用的事件系统独立运行,完美支持多实例共存
3.3.如何阻止冒泡
event.nativeEvent 可以拿到原生事件
scss
// 合成事件中阻止后续原生事件的发生
// 如果在合成的捕获阶段,那么后续的原生就无法触发,也就无法到root或document事件,那么后续的合成事件也就无法被触发
e.nativeEvent.stopImmediatePropagation();