React DOM 事件系统的初始化入口 ,在 createRoot 时调用,接收 React 根容器 DOM 作为入参,一次性给根容器挂载全部 React 支持的原生事件监听器,实现 React 事件委托(合成事件底层核心)
完整初始化流程
scss
应用启动:
createRoot(document.getElementById('root'))
→ listenToAllSupportedEvents(containerDiv)
│
├─ containerDiv['_reactListening' + rand] = true
│
├─ allNativeEvents = { click, keydown, mouseup, scroll, wheel, ... ~85 个 }
│
├─ click (非 nonDelegatedEvents):
│ ├─ listenToNativeEvent('click', false, containerDiv)
│ │ → addTrappedEventListener(containerDiv, 'click', 0, false)
│ │ → createEventListenerWrapperWithPriority(container, 'click', 0)
│ │ → getEventPriority('click') → DiscreteEventPriority
│ │ → wrapper = dispatchDiscreteEvent.bind(null, 'click', 0, container)
│ │ → containerDiv.addEventListener('click', wrapper, false) ← bubble
│ └─ listenToNativeEvent('click', true, containerDiv)
│ → addTrappedEventListener(containerDiv, 'click', IS_CAPTURE, true)
│ → containerDiv.addEventListener('click', wrapper, true) ← capture
│
├─ scroll (nonDelegatedEvents):
│ └─ listenToNativeEvent('scroll', true, containerDiv) ← 只有 capture!
│ → containerDiv.addEventListener('scroll', wrapper, true)
│
├─ touchstart (nonDelegatedEvents 无, 但 passive):
│ ├─ listenToNativeEvent('touchstart', false, containerDiv)
│ │ → isPassiveListener = true
│ │ → containerDiv.addEventListener('touchstart', wrapper, { passive: true })
│ └─ listenToNativeEvent('touchstart', true, containerDiv)
│ → container.addEventListener('touchstart', wrapper, { capture: true, passive: true })
│
└─ selectionchange:
├─ ownerDocument = document
└─ document['_reactListening' + rand] = true
└─ listenToNativeEvent('selectionchange', false, document)
→ document.addEventListener('selectionchange', wrapper, false)
绑定后 DOM 上的结果
ini
<div id="root" (containerDiv)>
├─ click (bubble): listener
├─ click (capture): listener
├─ keydown (bubble): listener
├─ keydown (capture): listener
├─ scroll (capture): listener ← 无 bubble
├─ wheel (bubble, passive): listener
├─ wheel (capture, passive): listener
├─ touchstart (bubble, passive): listener
├─ touchstart (capture, passive): listener
└─ ... (~85 个事件,每个事件 1-2 个 listener)
...
document:
└─ selectionchange (bubble): listener
源码解析
重复绑定拦截判断
javascript
const listeningMarker = '_reactListening' + Math.random().toString(36).slice(2);
if (!(rootContainerElement: any)[listeningMarker]) {
(rootContainerElement: any)[listeningMarker] = true;
}
作用
判断根容器 DOM 上是否已经挂载过 listeningMarker 标记
- 不存在标记 → 未绑定过事件,进入绑定逻辑,在根容器
DOM上挂载唯一标记,标记当前容器已完成全部事件绑定,后续再次调用函数会直接跳过绑定逻辑 - 已存在标记 → 直接跳过,避免重复执行绑定
设计意义
整个事件监听初始化是一次性的 :所有事件的 listener 在 rootContainer 创建时注册一次,之后 fiber 的 mount/update/unmount 通过 __reactFiber$<hash> 和 __reactProps$<hash> 从 DOM 节点获取当前事件处理函数,不需要重新注册
遍历所有原生支持事件
javascript
allNativeEvents.forEach(domEventName => {
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
// 委托事件 → rootContainer 上添加 bubble 监听
listenToNativeEvent(domEventName, false, rootContainerElement);
}
// 全部事件 → rootContainer 上添加 capture 监听
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
作用
- 循环遍历
React内置全部原生事件列表,逐个处理每种事件的捕获 / 冒泡监听 - 把
selectionchange事件从通用循环逻辑中剥离,不走下面的容器绑定逻辑,留到函数末尾单独绑定到document。因为selectionchange不冒泡,也不会在特定元素上触发,因为它在Document上触发,而不是在具体DOM元素上,无法通过根容器代理且仅能绑定在document节点,绑定在普通DOM容器不会触发 - 非委托事件:对可冒泡委托事件 ,在根容器注册冒泡阶段监听器
- 所有事件统一绑定捕获阶段监听:全部在根容器注册捕获阶段监听器
事件的监听策略
| 条件 | Bubble |
Capture |
示例 |
|---|---|---|---|
| 委托事件 | ✅ | ✅ | click: bubble + capture |
| 非委托事件 | ❌ | ✅ | scroll: capture only |
selectionchange |
❌(跳过) | ❌(跳过) | 单独处理 |
设计意义
React 19将事件委托到rootContainer。createRoot时一次性绑定所有事件,之后fiber的增删改不需要操作原生addEventListener,只需要在fiber上记录updateQueue,事件触发时再通过getListener从fiber树中查找allNativeEvents:React内部维护的所有支持的原生DOM事件名全集 (click、mouseover、input、change等)nonDelegatedEvents:Set集合,存储不支持事件委托、必须直接绑定到目标元素 的事件(如scroll、focus、blur等,这类事件不冒泡,无法通过根容器代理)React的合成事件模拟了捕获 + 冒泡两阶段:capture阶段:从根到目标(root→ ... →target)bubble阶段:从目标到根(target→ ... →root)
React通过capture listener实现capture阶段模拟:React在rootContainer上注册capture listener- 收到事件后,从
target向上遍历fiber树构建listener列表 - 按捕获顺序(逆序)执行
React通过bubble listener实现bubble阶段模拟:React在rootContainer上注册bubble listener- 收到事件后,从
target向上遍历fiber树构建listener列表 - 按冒泡顺序(正序)执行
处理 document 与 selectionchange
javascript
const ownerDocument =
rootContainerElement.nodeType === DOCUMENT_NODE
? rootContainerElement
: rootContainerElement.ownerDocument;
if (ownerDocument !== null) {
if (!ownerDocument[listeningMarker]) {
ownerDocument[listeningMarker] = true;
listenToNativeEvent('selectionchange', false, ownerDocument);
}
}
作用
- 获取根容器所属的
document对象- 如果根容器本身就是
document(SSR/ 特殊挂载场景),直接赋值自身; - 普通
#root容器,取其ownerDocument获取页面文档对象
- 如果根容器本身就是
document判空保护:兼容SSR、脱离文档流的DOM节点,防止ownerDocument为null时执行绑定报错,做空值安全防护document重复绑定拦截(同根容器标记逻辑):给document也挂载独立标记,防止重复绑定selectionchange,多根实例复用同一个document监听- 绑定特殊事件
selectionchange:在document上绑定冒泡阶段的selectionchange监听器
设计意义
selectionchange 是 React 事件系统中唯一一个不在 rootContainer 上监听 的事件。它作为特例在 listenToAllSupportedEvents 中单独处理,使用独立的 listeningMarker 防重,同一 document 的多个 root 共享同一个 document 监听
辅助函数
listenToNativeEvent
接收事件名、捕获 / 冒泡标识、绑定目标 DOM,组装事件标记位,调用底层真正注册监听的函数 addTrappedEventListener
javascript
export function listenToNativeEvent(
domEventName,
isCapturePhaseListener,
target,
) {
let eventSystemFlags = 0;
// 标记当前监听处于捕获阶段
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
}
作用
- 参数预处理,生成事件类型标记位 :初始化事件系统标记位,如果是捕获监听,打上捕获标记,后续在事件触发回调里,通过判断
eventSystemFlags区分当前是捕获监听还是冒泡监听,用来匹配onClickCapture/onClick - 转发调用底层注册函数
addTrappedEventListener- 创建
React内部封装的拦截回调(不是原生回调,里面包含合成事件构造、批量更新、优先级调度) - 执行原生
target.addEventListener(domEventName, 封装回调, isCapturePhaseListener) - 缓存监听器映射,用于后续卸载、事件清理
- 创建
设计思想
- 一次性委托 :
createRoot时将所有事件监听器绑定到rootContainer,此后fiber的mount/update/unmount不需要操作addEventListener,通过__reactProps$在fiber上获取当前handler capture + bubble双监听 :每个事件绑定两个监听器(非委托事件只绑capture),React通过从target到root的fiber树遍历 +listener列表正序/逆序执行模拟完整的两阶段事件流- 被动事件优化 :
touchstart/touchmove/wheel使用{ passive: true },浏览器承诺不调用preventDefault(),允许合成层在事件处理完成前就开始滚动,消除touchstart导致的滚动延迟 - 非委托事件单独处理 :
scroll,load,invalid,toggle等不冒泡或性能敏感事件只在capture阶段监听,避免冒泡到root的空遍历 selectionchange特殊路径 :唯一不在rootContainer上监听的事件------它不在元素上触发,在Document上触发;使用独立的listeningMarker防重- 随机标记防冲突 :
_reactListening+ 随机后缀避免与第三方库在DOM上的属性碰撞 - 惰性提取 + 批量
dispatch:listenToAllSupportedEvents只绑定listener,不关注handler。handler通过accumulateSinglePhaseListeners在事件触发时按fiber树 收集,绑定时不知道具体有哪些onClick