React的合成事件

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冒泡)

  1. 原生-document捕获
  2. 合成-父元素捕获
  3. 合成-子元素捕获
  4. 原生-root捕获
  5. 原生-父元素捕获
  6. 原生-子元素捕获
  7. 原生-子元素冒泡
  8. 原生-父元素冒泡
  9. 合成-子元素冒泡
  10. 合成-父元素冒泡
  11. 原生-root冒泡
  12. 原生-document冒泡 执行顺序-React16(事件委托到了document)

(原生document捕获-原生捕获-原生冒泡-合成捕获-合成冒泡-原生document冒泡)

  1. 原生-document捕获
  2. 原生-父元素捕获
  3. 原生-当前元素捕获
  4. 原生-当前元素冒泡
  5. 原生-父元素冒泡
  6. 合成-父元素捕获
  7. 合成-子元素捕获
  8. 合成-子元素冒泡
  9. 合成-父元素冒泡
  10. 原生-document冒泡

3.2.为什么React16到17要更改委托对象

为什么React16事件委托到document,而React17事件委托到root?

微前端场景下,多个独立 React 应用可能共存于同一页面。旧版全局 document 委托会导致事件冲突,而新版每个应用的事件系统独立运行,完美支持多实例共存

3.3.如何阻止冒泡

event.nativeEvent 可以拿到原生事件

scss 复制代码
// 合成事件中阻止后续原生事件的发生
// 如果在合成的捕获阶段,那么后续的原生就无法触发,也就无法到root或document事件,那么后续的合成事件也就无法被触发
e.nativeEvent.stopImmediatePropagation(); 
相关推荐
前端熊猫5 小时前
React Native (RN)的学习上手教程
学习·react native·react.js
@PHARAOH15 小时前
HOW - 缓存 React 自定义 hook 的所有返回值(包括函数)
前端·react.js·缓存
涵信17 小时前
第二节:React 基础篇-受控组件 vs 非受控组件
前端·javascript·react.js
HyaCinth20 小时前
Taro 数字滚动组件
javascript·react.js·taro
阿豪啊21 小时前
React入门(四)-全局路由以及mock数据模块化
react.js
会蹦的鱼1 天前
知识了解02——了解pnpm+vite+turbo+monorepo的完整构建步骤(react子项目)
前端·javascript·react.js
@PHARAOH1 天前
HOW - 如何测试 React 代码
前端·react.js·前端框架
涵信1 天前
第三节:React 基础篇-React组件通信方案
前端·javascript·react.js