认识
一、认识
React
基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等。在 React
中这套事件机制被称之为合成事件。
一、为什么要模拟事件?: 1. 跨浏览器兼容性, React
通过合成事件为所有浏览器提供了统一的事件 API
,屏蔽了不同浏览器之间原生事件实现的差异,让开发者能够以一致的方式处理事件,而无需考虑兼容性问题。2. 与 React
更新机制的整合, 针对不同事件引起的更新有着不同的优先级, 比如点击事件引起的更新优先级最高, 需要中断其他渲染任务, 来优先执行点击引起的渲染任务。那么, 通过模拟事件, React
能够基于 Scheduler
根据不同的优先级来调度更新任务, 来保证交互的快速响应, 从而优化页面性能。3. 事件委托与性能优化, 合成事件系统通常采用事件委托策略,即将所有事件监听器挂载在根容器上,再利用事件冒泡机制将事件分发到具体组件。这样可以大幅减少绑定在每个 DOM 元素上的事件处理器数量,降低内存占用,并提升整体性能。
二、初始化绑定事件: React
采用事件委托机制, 统一将事件绑定到一个容器上。React 16.8
为 Document
, React 17.x/18.x
为 React
应用的根容器。在 render
或者 createRoot
初始化过程中, 依次循环遍历 allNativeEvents
和 nonDelegatedEvents
, 通过 listenToNativeEvent
绑定两个集合中的事件绑定到根容器上, 分别对冒泡和捕获阶段进行事件绑定, 如果是不冒泡的事件, 只对该事件进行捕获阶段的绑定, 否则会对该事件冒泡和捕获阶段都绑定。无论是冒泡事件绑定还是捕获事件绑定, 他们的事件处理函数都是 dispatchEvent
-
allNativeEvents
是一个set
集合,保存了81
个浏览器常用事件 -
nonDelegatedEvents
也是一个集合,保存了浏览器中不会冒泡的事件,一般指的是媒体事件,比如pause
,play
,playing
等,还有一些特殊事件,比如cancel
,close
,invalid
,load
,scroll
。
三、事件处理函数存储: 在 Render CompleteWork
递阶段, 通过 fiber
创建真实 DOM
节点时, 将 fiber.pendingProps
存储到真实 DOM
节点中。其中包括 onClick
、onClickCapture
等事件。
四、事件触发工作流程: 当我们触发点击 onClick
或者 onChange
事件后, 会触发 dispatchEvent
函数。dispatchEvent
工作如下: 1. React
内部会从事件触发的目标 DOM
元素开始,沿着 DOM
树向上查找并收集注册在各层级上的事件回调函数。2. 构造合成事件, 在 JSX
中, 通过 onClick
、onChange
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.then
、setTimeout
等)中的更新也会被放入同一个批处理队列中, 等到时机成熟后, React
会统一将批量更新队列中的所有更新提交,这个过程会依次经过调度、渲染(即执行 Fiber
树的 Diff
及更新计算)以及 Commit
阶段,从而将所有变更高效地同步到真实 DOM
。
2.2 React 事件 Vs HTML DOM 事件?
-
React
事件 采用事件委托机制, 将所有的事件都绑定到了React
根容器 (React 16.x
时挂载在document
上,React 17.x
及以后挂载在容器上), 通过冒泡机制统一分发到各个组件, 这种设计既减少了内存消耗,也使多个React
根共存时互不干扰。HTML DOM
事件, 通常通过addEventListener
在各个DOM
节点上直接注册事件监听器,事件的绑定和处理都分散在各个节点上。 -
React
事件 通过SyntheticEvent
对原生事件进行封装和标准化,屏蔽了不同浏览器间的差异,使得开发者能够使用一致的API
(如preventDefault
、stopPropagation
等)来处理事件。HTML DOM
事件: 直接使用浏览器提供的原生事件,不同浏览器在某些事件属性和行为上可能存在细微差异,需要开发者自行处理兼容性问题。 -
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
冒泡