当前位置: 首页 > news >正文

React 源码揭秘 | 合成事件

前面的文章大致讲解了React具体的更新,渲染流程。这篇简单说一些细枝末节的点,即React合成事件的原理。

开始之前,依旧贴一下我的React实现

https://github.com/Gravity2333/My-React/tree/learn

为什么需要合成事件

合成事件 SyntheticEvent 设计的目的,用来统一不同浏览器之间事件处理的行为差异。在老版本浏览器(尤其IE)之间,事件名称,阻止默认行为的方法等等一些特性存在差异。 React对事件处理的逻辑进行一层封装,统一了事件的处理流程。

在新版本React中引入了优先级的概念,对于不同的事件,其优先程度是不一样的。比如用户的点击事件 click 就要比一些连续事件(比如scroll resize mousemove ) 的优先级更高,希望得到更快的处理,此时就可以通过在合成事件中引入调度器Scheduler 来按照优先级调度处理事件。

合成事件原理

传统的浏览器事件流动分为3个过程

1. 捕获阶段,事件从根节点html开始,到事件触发节点,依次触发对应事件

2. 目标阶段,事件到达元素本身

3. 冒泡阶段,事件从目标target向上流动回html元素

如下图:

React采用了事件委托的原理来模拟整个事件流动的过程。

事件委托

在需要监听事件节点的祖先节点上设置监听函数,把子元素的事件统一交给父元素去监听,通过事件冒泡机制识别目标,从而节省内存、提高性能、简化管理。

React 所管理渲染的节点通常会被挂载在 container容器上,所以用来委托的父元素也就是container元素节点。如下:

在我们调用 root.render时,render函数内部会调用一个 initEvent函数,其定义在SynsenticEvent.ts文件内部,如下:

// react-dom/index.ts/** 创建根节点的入口 */
export function createRoot(container: Container) {// 创建FiberRootNodeconst root = createContainer(container);return {render(element: ReactElement) {// TODO// 初始化合成事件initEvent(container);// 更新contianerreturn updateContainer(element, root);},};
}// events/SynrgeticEvent.ts/** 初始化合成事件 */
export function initEvent(container: Container) {/** 本质上就是在Container上的事件委托 */nativeEvents.forEach((nativeEvent) => {/** 对每种支持的原生事件 构建委托 */container.addEventListener(nativeEvent,dispatchSyntheticEvent.bind(null, container, nativeEvent));});
}

可以看到 initEvent函数内部在container元素上,对React所支持的所有事件类型都绑定了一个监听函数,nativeEvents变量被定义在 event/events.ts中,包含了浏览器中所有的原生事件名称

// event/events.ts
/** 可以代理的原生事件 */
export const nativeEvents = ["click","contextmenu","dblclick","mousedown","mouseenter","mouseleave","mousemove","mouseout",
...
]

也就是说,对于每种事件类型,React都在container节点上,绑定一个统一的处理函数。container的所有子节点(React管理的所有节点)触发的同类型事件,都由container上的一个统一事件处理函数处理。

dispatchSyntheticEvent

处理函数 dispatchSyntheticEvent 用来模拟事件的 捕获 目标 冒泡 三个阶段的逻辑,其主要过程包含

1. 收集从container节点 -> 事件触发节点 路径上所有节点所绑定的对应事件的处理函数

2. 通过MonkeyPatch扩展stopPropagation 函数,用来控制是否继续传递事件

3. 按照捕获 目标 冒泡三个阶段,调用路径上的处理函数

具体实现如下:

// event/syntheticEvent.ts
/*** 触发合成事件* @param container 委托的container* @param eventType 原生事件类型* @param event 事件对象*/
function dispatchSyntheticEvent(container: Container,eventType: string,event: Event
) {// 收集事件路径上的代理事件const collectedEvents = collectEvents(container, eventType, event);// 没有任何代理事件,提前结束if (collectedEvents.bubbleCallbacks?.length === 0 &&collectedEvents.captureCallbacks?.length == 0) {return;}// 代理阻止冒泡事件event[stopPropagationKey] = false;// 原始的 stopPropagation 函数const originStopPropagation = event.stopPropagation;// 使用Monkey Patch打补丁 包装一层 stopPropagation函数event.stopPropagation = () => {event[stopPropagationKey] = true;originStopPropagation();};// 执行捕获事件triggerEventListeners(collectedEvents.captureCallbacks, event);if (!event[stopPropagationKey]) {triggerEventListeners(collectedEvents.bubbleCallbacks, event);}
}
collectEvents 收集事件

React定义了一个 CollectedEvents对象,用来存储从container到target沿途收集到的事件处理函数,其ts定义如下

type CollectedEvents = {captureCallbacks: SyntheticEventListener[];bubbleCallbacks: SyntheticEventListener[];
};

可以看到,其中包含了captureCallbacks 和 bubbleCallbacks两个数组,分别存储捕获处理函数和冒泡处理函数,这个对象就是作为collectEvents的返回结果,具体收集过程如下:

/*** 从target到source 收集冒泡/捕获事件* @param source 一般为代理节点 container* @param eventType 事件类型 比如 click hover ...* @param event 事件对象本身* @returns*/
function collectEvents(source: Element,eventType: string,event: Event
): CollectedEvents {/** 收集的事件对象 */const events: CollectedEvents = {captureCallbacks: [],bubbleCallbacks: [],};// 收集顺序 从 target -> source 初始化收集节点为 target (事件触发节点)let currentNode = event.target as Element;// 根据事件类型,获取react合成事件代理的回调函数名 比如 click => [onClickCapture,onClick]const reactEvent = reactEvents[eventType];// 没有匹配到 合成事件无法处理!if (!reactEvent) return events;while (currentNode !== source) {// 从target收集到source// 当前收集节点的 所有属性 propsconst nodeProps = getFiberProps(currentNode);// 收集事件处理函数if (nodeProps[reactEvent[1]]) {// 冒泡事件events.bubbleCallbacks.push(nodeProps[reactEvent[1]]);}if (nodeProps[reactEvent[0]]) {// 捕获事件, 注意捕获顺序是反向收集的events.captureCallbacks.unshift(nodeProps[reactEvent[0]]);}currentNode = currentNode.parentNode as Element;}return events;
}

CollectEvents函数内部会先获得当前收集的事件所对应的事件处理函数,比如 我们通过 onClick/onClickCapture来绑定某个节点的click事件的冒泡捕获阶段回调。

这个事件通过ReactEvents字典获得,其中包含了所有原生事件对应的 捕获 和 冒泡函数名称(即我们通常绑定事件所用的属性名) 如下

// React 合成事件对象
export const reactEvents = {click: ["onClickCapture", "onClick"],contextmenu: ["onContextMenuCapture", "onContextMenu"],dblclick: ["onDoubleClickCapture", "onDoubleClick"],mousedown: ["onMouseDownCapture", "onMouseDown"],mouseenter: ["onMouseEnterCapture", "onMouseEnter"],mouseleave: ["onMouseLeaveCapture", "onMouseLeave"],...
]

拿到对应的事件函数名,就开始从目标元素 target 向上收集到 container元素,每经过一个路径上的元素节点,都会检查,其上是否绑定了当前收集事件对应的处理函数

我们对某个节点绑定一个事件 比如onClick事件,那么对应的回调函数会被保存在这个DOM节点的 __prop属性上,我们可以通过 __prop来拿到一个DOM节点上所绑定的所有属性信息,包括事件处理函数!

/** 合成事件 */
const elementPropsKey = "__props";/** 判断是否为事件 */
const isEvent = (key) => reactEventSet.has(key);/** 获取事件 */
export function getFiberEvents(node: Element) {const _prop = node[elementPropsKey];return _prop.filter(isEvent);
}

如果存在对应的捕获处理函数(onClickCapture)那么就会从captureEvents数组的前面 unshift入数组,如果存在冒泡处理函数 (onClick) 就会从后方push到bubbleEvents内部,如下:

最终收集结果events如下

此时,按顺序便利 captureEvents和bubbleEvents两个数字,就是事件捕获 目标 冒泡的顺序了。

处理stopPropagation

收集完事件处理函数之后,就可以按顺序执行事件处理函数了,但是在这之前,react需要知道什么时候停止事件的传播。

在传统dom事件监听处理中,event事件上提供了一个stopPropagation 函数,调用后,浏览器内部会停止事件的传播。

但是在我们模拟的合成事件中,我们无法知道何时停止传播,所以就需要在事件上绑定一个 是否停止传播的状态,当某个事件处理函数调用了stopPropagation之后,改变这个状态即可。

这就需要我们用Monkey Patch的方式,对stopPropagation函数进行一层包装替换:

/** 阻止冒泡Key */
const stopPropagationKey = "__stopPropagation";// 代理阻止冒泡事件event[stopPropagationKey] = false;// 原始的 stopPropagation 函数const originStopPropagation = event.stopPropagation;// 使用Monkey Patch打补丁 包装一层 stopPropagation函数event.stopPropagation = () => {event[stopPropagationKey] = true;originStopPropagation();};

当我们调用替换之后的stopPropagation函数后,会先改变 event.__stopPropagation属性的值,再调用原生的stopPropagation ` 函数

triggerEvents 执行事件处理函数

最后一步,就是顺序执行事件处理函数,并且在每执行完一个事件处理函数之后,检查当前的 __stopPropagation属性是否为true,如果是就停止执行过程

  // 执行捕获事件triggerEventListeners(collectedEvents.captureCallbacks, event);if (!event[stopPropagationKey]) {// 执行冒泡事件triggerEventListeners(collectedEvents.bubbleCallbacks, event);}/* 执行事件 */
function triggerEventListeners(listeners: SyntheticEventListener[],event: Event
) {for (let i = 0; i < listeners.length; i++) {const listener = listeners[i];// 获得时间对应的优先级,并且交给scheduler调度scheduler.runWithPriority(eventTypeToSchedulerPriority(event.type), () => {listener(event);});// 结束传播if (!event[stopPropagationKey]) {break;}}
}

优先级的处理

合成事件的一大优势就是给不同类型的事件引入了不同的处理优先级。

比如,用户的点击事件click和缩放事件resize同时触发,哪个重要一点?

传统的事件处理,浏览器采用谁先触发,谁先被放到宏任务队列中,谁先执行的方式,就会有些问题。

由于没有优先级的概念,对于resize 拖拽这种每秒可能触发几百次的 连续事件,可能会影响到点击事件的响应时间,可能用户点击之后需要先等待连续事件处理完才能得到回馈,这就造成了用户体验的下降。

react合成事件对不同的原生事件进行分类,根据优先级不同将事件分成

 立即执行事件 (click )> 用户阻塞事件(input) > 可以推迟的事件(连续事件 scroll resize)

这个分类被保存在 event/events.ts内


/** 事件转优先级 */
export function eventTypeToSchedulerPriority(eventType: string) {switch (eventType) {case "click":case "keydown":case "keyup":case "keydown":case "keypress":case "keyup":case "focusin":case "focusout":return PriorityLevel.IMMEDIATE_PRIORITY;case "input":case "change":case "submit":case "focus":case "blur":case "select":case "drag":case "drop":case "pause":case "play":case "waiting":case "ended":case "canplay":case "canplaythrough":return PriorityLevel.USER_BLOCKING_PRIORITY;case "scroll":case "resize":case "mousemove":case "mouseenter":case "mouseleave":case "touchstart":case "touchmove":case "touchend":return PriorityLevel.NORMAL_PRIORITY;case "abort":case "load":case "loadeddata":case "loadedmetadata":case "error":case "durationchange":return PriorityLevel.LOW_PRIORITY;default:return PriorityLevel.IDLE_PRIORITY;}
}

由上到下优先级逐渐降低,对于click这种立即执行优先级的函数,会被调度器同步执行,确保不被阻塞!

在triggerEvents函数中,会根据事件的类型,通过上述分类匹配到对应的优先级,将处理函数根据优先级交给scheduler调度器进行调度运行

    // 获得时间对应的优先级,并且交给scheduler调度scheduler.runWithPriority(eventTypeToSchedulerPriority(event.type), () => {listener(event);});

通过这种方式,就能保证事件根据对应的优先级被处理,并且先处理高优先级事件触发的渲染re

我们就完成了对原生事件的流动和处理的模拟。

http://www.dtcms.com/a/461329.html

相关文章:

  • 如何处理旧 iPhone:安全地回收或重新利用
  • 过年做那些网站能致富网页制作培训苏州
  • 公司网站百度地图微信小程序商城源代码
  • 【征文计划】AI+AR生态新未来,Rokid核心技术实战解析
  • AI 驱动的 AR眼镜巡检技术方案:让工业缺陷识别更精准高效|阿法龙XR云平台​
  • JFM9VU3P开发板/国产FPGA/ QSFP+ 40G 光纤接口
  • 使用as断言可能会掩盖类型错误,更安全的方式是:
  • 安宝特方案丨软硬件双升级的AR智能仓储物流解决方案
  • 网站防护怎么做Wordpress 主题简化
  • 赤峰市做网站公司网站建设考试知识点
  • 《Qt应用开发》笔记
  • 字节面试题:大模型LoRA微调矩阵参数初始化
  • 通过 SSH 远程连接 docker 容器
  • 什么网站可以用手机做兼职赚钱吗威海人才招聘网官网
  • 《穿透式理解C++继承:虚函数表、对象切片与多重继承陷阱》
  • 网站建设属于技术服务吗服装网站设计模板
  • 口碑好的肿瘤电场疗法领先厂家
  • ubuntu
  • 密码学入门:从古典加密到现代网络安全
  • 2003服务器建设网站济南多语言网站建设
  • 技术人力外派公司的数据驱动运营:通过量化管理保障服务品质与客户成功
  • pc 不在 sudoers 文件中。此事将被报告。
  • 遗留系统微服务改造(一):遗留系统改造策略与实战场景分析
  • 微服务基础:远程调用的基本使用详解
  • 实时性、数据覆盖范围和易用性的优质金融数据源API推荐
  • 从零开始:在VSCode中配置现代OpenGL开发环境(MinGW + GLFW + GLAD
  • 消费金融系统-利息核算与财务核算
  • 寻梦数据空间 | 政策篇:构筑数据基座的国家战略与行动蓝图
  • 长春火车站照片十佳工业设计公司
  • Shell 脚本01