跳到主要内容

认识

一、认识


React 基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等。在 React 中这套事件机制被称之为合成事件。

一、为什么要模拟事件?: 1. 跨浏览器兼容性, React 通过合成事件为所有浏览器提供了统一的事件 API,屏蔽了不同浏览器之间原生事件实现的差异,让开发者能够以一致的方式处理事件,而无需考虑兼容性问题。2. React 更新机制的整合, 针对不同事件引起的更新有着不同的优先级, 比如点击事件引起的更新优先级最高, 需要中断其他渲染任务, 来优先执行点击引起的渲染任务。那么, 通过模拟事件, React 能够基于 Scheduler 根据不同的优先级来调度更新任务, 来保证交互的快速响应, 从而优化页面性能。3. 事件委托与性能优化, 合成事件系统通常采用事件委托策略,即将所有事件监听器挂载在根容器上,再利用事件冒泡机制将事件分发到具体组件。这样可以大幅减少绑定在每个 DOM 元素上的事件处理器数量,降低内存占用,并提升整体性能。

二、初始化绑定事件: React 采用事件委托机制, 统一将事件绑定到一个容器上。React 16.8Document, React 17.x/18.xReact 应用的根容器。在 render 或者 createRoot 初始化过程中, 依次循环遍历 allNativeEventsnonDelegatedEvents, 通过 listenToNativeEvent 绑定两个集合中的事件绑定到根容器上, 分别对冒泡和捕获阶段进行事件绑定, 如果是不冒泡的事件, 只对该事件进行捕获阶段的绑定, 否则会对该事件冒泡和捕获阶段都绑定。无论是冒泡事件绑定还是捕获事件绑定, 他们的事件处理函数都是 dispatchEvent

  • allNativeEvents 是一个 set 集合,保存了 81 个浏览器常用事件

  • nonDelegatedEvents 也是一个集合,保存了浏览器中不会冒泡的事件,一般指的是媒体事件,比如pauseplayplaying 等,还有一些特殊事件,比如 cancelcloseinvalidloadscroll

三、事件处理函数存储: 在 Render CompleteWork 递阶段, 通过 fiber 创建真实 DOM 节点时, 将 fiber.pendingProps 存储到真实 DOM 节点中。其中包括 onClickonClickCapture 等事件。

四、事件触发工作流程: 当我们触发点击 onClick 或者 onChange 事件后, 会触发 dispatchEvent 函数。dispatchEvent 工作如下: 1. React 内部会从事件触发的目标 DOM 元素开始,沿着 DOM 树向上查找并收集注册在各层级上的事件回调函数。2. 构造合成事件, 在 JSX 中, 通过 onClickonChange onScroll 注册的事件, 在 React 内部保存着跟原生事件的对应关系, 比如 onChange , 对应 ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'] 等多个原生事件, React 会根据这些原生事件分别收集相应的回调, 然后整合到捕获和冒泡的回调队列中。3. 创建事件对象 SyntheticEvent, SyntheticEvent 对象对浏览器原生事件进行了封装和标准化,屏蔽了不同浏览器间的差异,使得开发者可以用一致的 API 来处理事件,例如支持冒泡、捕获和阻止默认行为等。4. 收集完毕后,React 会依次通过内部的 Scheduler 按照当前事件对应的优先级来调度这些回调的执行。这确保了在并发模式下,用户交互等高优先级任务能够被及时响应。5. 批量更新, 在调用每个事件回调时, React 会建立一个批量更新的上下文。所有由事件回调产生的状态更新, 会被放入同一个批处理队列中, 事件回调执行完后, React 会统一将批量更新队列中的所有更新提交,这个过程会依次经过调度、渲染(即执行 Fiber 树的 Diff 及更新计算)以及 Commit 阶段,从而将所有变更高效地同步到真实 DOM

二、问题


2.1 批量更新机制

React 17.x 批量更新: 在 React 17 中,所有由事件回调(以及生命周期方法、React 内部触发的更新)产生的状态更新会被放入同一个批处理队列中, 等到时机成熟后, React 会统一将批量更新队列中的所有更新提交,这个过程会依次经过调度、渲染(即执行 Fiber 树的 Diff 及更新计算)以及 Commit 阶段,从而将所有变更高效地同步到真实 DOM

React 18.x 批量更新扩展: React 18 进一步扩展了自动批量更新的范围,不仅限于事件回调中产生的更新,还会对异步任务(例如 Promise.thensetTimeout 等)中的更新也会被放入同一个批处理队列中, 等到时机成熟后, React 会统一将批量更新队列中的所有更新提交,这个过程会依次经过调度、渲染(即执行 Fiber 树的 Diff 及更新计算)以及 Commit 阶段,从而将所有变更高效地同步到真实 DOM

2.2 React 事件 Vs HTML DOM 事件?

  1. React 事件 采用事件委托机制, 将所有的事件都绑定到了 React 根容器React 16.x 时挂载在 document 上,React 17.x 及以后挂载在容器上), 通过冒泡机制统一分发到各个组件, 这种设计既减少了内存消耗,也使多个 React 根共存时互不干扰。HTML DOM 事件, 通常通过 addEventListener 在各个 DOM 节点上直接注册事件监听器,事件的绑定和处理都分散在各个节点上。

  2. React 事件 通过 SyntheticEvent 对原生事件进行封装和标准化,屏蔽了不同浏览器间的差异,使得开发者能够使用一致的 API(如 preventDefaultstopPropagation 等)来处理事件。HTML DOM 事件: 直接使用浏览器提供的原生事件,不同浏览器在某些事件属性和行为上可能存在细微差异,需要开发者自行处理兼容性问题。

  3. React 事件 在各个 JSX 上注册的事件处理函数并不是真正的事件处理函数, 将这些事件函数统一收集供 dispatchEvent 调用。而 dispatchEvent 是真正的事件处理函数。因此, 在 JSX 上注册的事件处理函数不能通过 return false 来阻止默认事件。

2.3 React 16.8 事件 Vs React 17.x 18.x ?

React.js 16.x 顶层容器为 Document。因此, React 执行大多数事件都会调用 document.addEventListener()。触发事件后, 事件流为: Document 捕获 -> DOM 原生捕获 -> DOM 原生冒泡 -> React 捕获 -> React 冒泡 -> Document 冒泡React.js 16.x 捕获阶段和冒泡阶段的事件都是模拟的,本质上都是在冒泡阶段执行的。一定程度上,不符合事件流的执行时机。

React.js 17.x 顶层容器为 root。因此, React 执行大多数事件都会调用 rootNode.addEventListener()。触发事件后, 事件流为: Document 捕获 -> React 合成事件捕获 -> DOM 原生捕获 -> DOM 原生冒泡 -> React 合成事件冒泡 -> Document 冒泡

React.js 18.x 顶层容器为 root。因此, React 执行大多数事件都会调用 rootNode.addEventListener()。触发事件后, 事件流为: Document 捕获 -> React 合成事件捕获 -> DOM 原生捕获 -> DOM 原生冒泡 -> React 合成事件冒泡 -> Document 冒泡

2.4 说说 React 事件和原生事件的执行顺序?

React.js 16.x 顶层容器为 Document。因此, React 执行大多数事件都会调用 document.addEventListener()。触发事件后, 事件流为: Document 捕获 -> DOM 原生捕获 -> DOM 原生冒泡 -> React 捕获 -> React 冒泡 -> Document 冒泡React.js 16.x 捕获阶段和冒泡阶段的事件都是模拟的,本质上都是在冒泡阶段执行的。一定程度上,不符合事件流的执行时机。

React.js 17.x 顶层容器为 root。因此, React 执行大多数事件都会调用 rootNode.addEventListener()。触发事件后, 事件流为: Document 捕获 -> React 合成事件捕获 -> DOM 原生捕获 -> DOM 原生冒泡 -> React 合成事件冒泡 -> Document 冒泡

React.js 18.x 顶层容器为 root。因此, React 执行大多数事件都会调用 rootNode.addEventListener()。触发事件后, 事件流为: Document 捕获 -> React 合成事件捕获 -> DOM 原生捕获 -> DOM 原生冒泡 -> React 合成事件冒泡 -> Document 冒泡

参考资料


「React进阶」一文吃透react事件原理