React 合成事件系统最底层 DOM 监听器创建与绑定核心函数
- 生成
React封装的高优先级事件回调 - 自动区分
touch/wheel事件开启passive优化 - 根据捕获 / 冒泡 + 是否
passive分流调用对应底层绑定API - 生成解绑句柄,供一次性监听自动销毁
完整调用链路全览
──────────────────────────────────────────────────────────────────────
createRoot(container)
──────────────────────────────────────────────────────────────────────
│
▼
listenToAllSupportedEvents(container)
│
├── (container._reactListening = true)
│
├── allNativeEvents.forEach('click'):
│ ├── 'click' ∉ nonDelegatedEvents
│ │ └── listenToNativeEvent('click', false, container)
│ │ └── addTrappedEventListener(container, 'click', 0, false)
│ │ ├── createEventListenerWrapperWithPriority(container, 'click', 0)
│ │ │ └── getEventPriority('click') → DiscreteEventPriority
│ │ │ └── dispatchDiscreteEvent.bind(null, 'click', 0, container)
│ │ ├── passiveBrowserEventsSupported? Yes, but click ∈ [touchstart,touchmove,wheel]? No
│ │ │ └── isPassiveListener = undefined
│ │ ├── isCapturePhaseListener? false → addEventBubbleListener
│ │ │ └── container.addEventListener('click', wrapper, false)
│ │ └── return listener (unsubscribe)
│ │
│ └── listenToNativeEvent('click', true, container)
│ └── addTrappedEventListener(container, 'click', IS_CAPTURE_PHASE, true)
│ ├── dispatchDiscreteEvent.bind(null, 'click', IS_CAPTURE_PHASE, container)
│ ├── isCapturePhaseListener? true → addEventCaptureListener
│ │ └── container.addEventListener('click', wrapper, true)
│ └── return listener
│
├── allNativeEvents.forEach('touchstart'):
│ ├── listenToNativeEvent('touchstart', false, container)
│ │ └── addTrappedEventListener(container, 'touchstart', 0, false)
│ │ ├── isPassiveListener = true (domEventName === 'touchstart')
│ │ ├── isCapturePhaseListener? false, isPassiveListener? true
│ │ │ └── addEventBubbleListenerWithPassiveFlag(container, 'touchstart', wrapper, true)
│ │ │ └── container.addEventListener('touchstart', wrapper, { passive: true })
│ │ └── return listener
│ │
│ └── listenToNativeEvent('touchstart', true, container)
│ └── addTrappedEventListener(container, 'touchstart', IS_CAPTURE_PHASE, true)
│ ├── isCapturePhaseListener? true, isPassiveListener? true
│ │ └── addEventCaptureListenerWithPassiveFlag(container, 'touchstart', wrapper, true)
│ │ └── container.addEventListener('touchstart', wrapper, { capture: true, passive: true })
│ └── return listener
│
└── ownerDocument = document
└── listenToNativeEvent('selectionchange', false, document)
└── addTrappedEventListener(document, 'selectionchange', 0, false)
├── getEventPriority('selectionchange') → DiscreteEventPriority
├── isPassiveListener? false (non-passive)
└── document.addEventListener('selectionchange', wrapper, false)
──────────────────────────────────────────────────────────────────────
mount(<img onLoad={fn} />)
──────────────────────────────────────────────────────────────────────
│
└── ReactDOMComponent.finalizeInitialChildren
└── listenToNonDelegatedEvent('load', imgElement)
├── listenerSet = imgElement.__reactEvents || new Set()
├── listenerSetKey = "load__bubble"
├── Set has "load__bubble"? No
│
└── addTrappedEventListener(imgElement, 'load', IS_NON_DELEGATED, false)
├── createEventListenerWrapperWithPriority(imgElement, 'load', IS_NON_DELEGATED)
│ └── getEventPriority('load') → DefaultEventPriority
│ └── dispatchEvent.bind(null, 'load', IS_NON_DELEGATED, imgElement)
├── isPassiveListener = undefined (load ∉ [touchstart,touchmove,wheel])
└── imgElement.addEventListener('load', wrapper, false)
└── listenerSet.add("load__bubble")
源码解析
步骤 ①:创建优先级包装器
生成非原生、带 React 调度能力的拦截回调:原生 DOM 触发事件时,不会直接执行组件 onClick,而是先走这个 wrapper。把原生 DOM 事件与 ``React 内部渲染调度、合成事件机制解耦,所有底层监听统一使用标准化包装函数,上层不用关心调度细节
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
步骤 ②:被动事件优化
浏览器支持 passive 配置时,滚动 / 触摸类事件强制设为 passive: true。自动做滚动性能优化,开发者无需手动配置,抹平浏览器滚动卡顿缺陷
let isPassiveListener = undefined;
if (passiveBrowserEventsSupported) {
if (
domEventName === 'touchstart' ||
domEventName === 'touchmove' ||
domEventName === 'wheel'
) {
isPassiveListener = true;
}
}
// 特性检测:浏览器是否支持 { passive: true } 选项
if (canUseDOM) {
try {
const options = {};
Object.defineProperty(options, 'passive', {
get: function () {
passiveBrowserEventsSupported = true; // getter 触发 → 浏览器支持
},
});
window.addEventListener('test', options, options);
window.removeEventListener('test', options, options);
} catch (e) {
passiveBrowserEventsSupported = false;
}
}
检测原理
addEventListener 第三个参数可以传入 { passive: true }。如果浏览器支持,options.passive 的 getter 会被触发,passiveBrowserEventsSupported 设为 true。如果浏览器不支持此选项(如 IE ≤ 11),catch 块设为 false
设计意义
Chrome 56+ 将 touchstart/touchmove/wheel 的 document 级监听器默认设为 { passive: true }(因为开发者几乎不会在 touch 事件上调用 preventDefault 但必须等待 JS 执行判断是否调用)。React 19 不把事件绑到 document 而绑到 rootContainer,手动模拟这个 passive 行为以获得相同性能收益
步骤 ③:选择 addEventListener 重载
根据捕获 / 冒泡 + 是否 passive 分流到 4 个底层绑定工具,统一管理 DOM addEventListener 参数差异,保存解绑函数,支撑一次性监听自动移除,为后续全局卸载根容器事件提供基础能力
if (isCapturePhaseListener) {
// 捕获阶段
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
targetContainer, domEventName, listener, isPassiveListener,
);
} else {
unsubscribeListener = addEventCaptureListener(
targetContainer, domEventName, listener,
);
}
} else {
// 冒泡阶段
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
targetContainer, domEventName, listener, isPassiveListener,
);
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer, domEventName, listener,
);
}
}
export function addEventBubbleListener(
target,
eventType,
listener,
) {
target.addEventListener(eventType, listener, false);
return listener;
}
export function addEventCaptureListener(
target,
eventType,
listener,
) {
target.addEventListener(eventType, listener, true);
return listener;
}
export function addEventCaptureListenerWithPassiveFlag(
target,
eventType,
listener,
passive,
) {
target.addEventListener(eventType, listener, {
capture: true,
passive,
});
return listener;
}
export function addEventBubbleListenerWithPassiveFlag(
target,
eventType,
listener,
passive,
) {
target.addEventListener(eventType, listener, {
passive,
});
return listener;
}
四种组合
- 捕获 +
passive(touch/wheel) - 捕获 + 无
passive(普通事件如clickCapture) - 冒泡 +
passive(touch/wheel常规监听) - 冒泡 + 无
passive(常规click/input等)
isCapturePhaseListener | isPassiveListener | 调用的函数 | DOM 参数 |
|---|---|---|---|
true | true | addEventCaptureListenerWithPassiveFlag | { capture: true, passive: true } |
true | undefined | addEventCaptureListener | true |
false | true | addEventBubbleListenerWithPassiveFlag | { passive: true } |
false | undefined | addEventBubbleListener | false |
辅助函数
创建优先级事件监听器:createEventListenerWrapperWithPriority
export function createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
) {
const eventPriority = getEventPriority(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
作用
- 调用
getEventPriority根据事件名称区分事件优先级,React并发渲染调度的核心依据DiscreteEventPriority:离散高优事件(click、keydown、input、focus 等,用户即时交互,需要同步响应,可打断渲染)ContinuousEventPriority:连续高频事件(mousemove、scroll、touchmove 等,持续触发,低打断权重)DefaultEventPriority:默认普通优先级(其余事件)
- 按事件类型分流调度逻辑,实现
React并发渲染的优先级机制,不同交互行为拥有不同渲染抢占能力- 离散高优事件:使用
dispatchDiscreteEvent。标记离散更新、同步刷新、允许中断后台渲染,保证点击、输入即时响应 - 连续高频事件:使用
dispatchContinuousEvent。走连续调度,合并多次更新,避免高频触发造成页面卡顿 - 默认 / 其他事件:兜底
dispatchEvent,标准合成事件流程
- 离散高优事件:使用
bind预绑定固定参数,返回最终监听回调:把静态不变的配置(事件名、捕获标记、根容器)提前固化,不用每次事件触发再传递 / 查找,减少运行时计算开销;同时统一分发函数入参规范,下层分发逻辑无需额外从DOM读取信息- 预固化三个前置参数:
domEventName、eventSystemFlags、targetContainer - 当浏览器触发事件执行回调时,原生
Event对象会作为第四个参数传入分发函数
- 预固化三个前置参数:
事件优先级映射(getEventPriority)
| 优先级 | 代表事件 | 包装器 |
|---|---|---|
DiscreteEventPriority | click, keydown, focusin, input, mousedown, touchstart, submit, toggle, beforetoggle, cancel, close, paste, copy, cut, contextmenu, auxclick, dblclick, dragend, dragstart, drop, invalid, keypress, keyup, mouseup, pause, play, pointercancel, pointerdown, pointerup, ratechange, reset, seeked, touchcancel, touchend, volumechange, change, selectionchange, beforeblur, afterblur, blur, focus, hashchange, popstate, select, selectstart, beforeinput, fullscreenchange, fullscreenerror | dispatchDiscreteEvent |
ContinuousEventPriority | mousemove, mouseout, mouseover, pointermove, pointerout, pointerover, drag, dragenter, dragexit, dragleave, dragover, resize, scroll, touchmove, wheel, mouseenter, mouseleave, pointerenter, pointerleave | dispatchContinuousEvent |
DefaultEventPriority | load, error, animationend, transitionend, message(特定调度优先级)等不匹配上述分类的事件 | dispatchEvent |
离散事件的调度包装器:dispatchDiscreteEvent
- 临时提升渲染优先级,保障交互即时性:针对
click/input/keydown等离散用户交互,把事件内所有状态更新强制设为最高优先级,能够打断后台正在执行的低优先级渲染,解决点击延迟、输入卡顿问题,是React并发渲染交互流畅性的核心保障 - 隔离
startTransition低优上下文:强制清空全局Transition标记,确保离散交互不受startTransition降级逻辑影响,紧急操作永远优先渲染 - 上下文压栈 + 自动恢复,无全局污染:采用先缓存现场 → 修改全局环境 → 执行事件逻辑 → 无论成败强制还原的范式,不会篡改外部代码的优先级、
Transition环境,多个事件嵌套、并发执行时上下文互不干扰
function dispatchDiscreteEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
const prevTransition = ReactSharedInternals.T;
ReactSharedInternals.T = null;
const previousPriority = getCurrentUpdatePriority();
try {
setCurrentUpdatePriority(DiscreteEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
setCurrentUpdatePriority(previousPriority);
ReactSharedInternals.T = prevTransition;
}
}
设计意义
即使当前代码处在 startTransition 内部,点击 / 输入这类紧急交互也要走高优渲染,不能被降级延迟,事件执行过程中产生的所有 setState 更新,都会标记为高优,可以中断正在进行的后台渲染,保证交互即时响应
连续事件的调度包装器:dispatchContinuousEvent
- 设置连续事件专属调度优先级,平衡流畅性与性能针对滚动、鼠标移动、拖拽、滚轮等每秒多次触发的高频事件,使用中等优先级:
- 不会像离散点击那样强行抢占渲染;
- 多个连续更新会被调度器合并,减少重复渲染开销;
- 当同时发生点击输入时,离散事件可以打断滚动渲染,保证用户主动交互优先
- 剥离
startTransition低优上下文:清空全局Transition标记,滚动相关更新不会被当作延迟渲染任务,避免滚动滞后、画面掉帧 - 安全隔离全局调度环境,无副作用污染:采用缓存现场 → 临时覆盖 → 最终强制还原的栈式上下文模式,单次事件执行仅局部生效,函数退出后全局状态完全复原,异常场景也不会破坏全局调度规则
function dispatchContinuousEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
const prevTransition = ReactSharedInternals.T;
ReactSharedInternals.T = null;
const previousPriority = getCurrentUpdatePriority();
try {
setCurrentUpdatePriority(ContinuousEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
setCurrentUpdatePriority(previousPriority);
ReactSharedInternals.T = prevTransition;
}
}
设计意义
滚动、鼠标移动这类持续交互,不降级为 transition 更新,使用专属连续优先级(优先级低于点击、输入等离散事件),后台渲染任务遇到离散交互会被直接抢占中断,连续事件之间会合并调度,减少重复渲染,防止高频事件卡死页面。高频滚动事件不抢占主线程,避免短时间大量渲染阻塞页面
事件分发中枢控制器:dispatchEvent
针对 SSR 水合阻塞场景做分流处理
- 无未水合
DOM:直接执行完整事件流程 - 高频连续事件遇阻塞:暂存队列、丢弃本次触发
- 捕获阶段离散交互事件遇阻塞:同步强制水合组件,再执行事件
- 其余普通阻塞场景:直接执行事件(不等待水合)
export function dispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
) {
// 标记 React 合成事件系统是否启用;关闭则直接跳过所有事件处理
if (!_enabled) {
return;
}
// 查找当前触发事件的 DOM 节点链上,还未完成水合(hydrate)的 DOM 元素;返回对应 DOM,无阻塞节点返回 null
let blockedOn = findInstanceBlockingEvent(nativeEvent);
if (blockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst, // 事件目标 DOM 对应的 Fiber 实例
targetContainer,
);
// 清空当前连续事件缓存队列,丢弃过期事件
clearIfContinuousEvent(domEventName, nativeEvent);
return;
}
if (
// 连续事件(scroll/mousemove/touchmove)入队
queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)
) {
nativeEvent.stopPropagation();
return;
}
clearIfContinuousEvent(domEventName, nativeEvent);
if (
eventSystemFlags & IS_CAPTURE_PHASE &&
// 判断该离散事件是否必须等待水合完成才能执行(click、input、keydown 等用户强交互)
isDiscreteEventThatRequiresHydration(domEventName)
) {
while (blockedOn !== null) {
const fiber = getInstanceFromNode(blockedOn);
if (fiber !== null) {
attemptSynchronousHydration(fiber);
}
const nextBlockedOn = findInstanceBlockingEvent(nativeEvent);
if (nextBlockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst,
targetContainer,
);
}
if (nextBlockedOn === blockedOn) {
break;
}
blockedOn = nextBlockedOn;
}
if (blockedOn !== null) {
nativeEvent.stopPropagation();
}
return;
}
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}
作用
- 事件系统总开关拦截:全局关闭合成事件系统时,直接终止事件分发,不执行任何后续逻辑
- 调用
findInstanceBlockingEvent查找是否存在未水合阻塞DOM:从原生事件触发目标向上遍历DOM树,查找对应Fiber未完成水合的DOM节点 - 无任何未水合阻塞节点(正常流程):直接执行事件,传入目标
Fiber,清空连续事件队列- 调用核心事件执行函数
dispatchEventForPluginEventSystem,传入目标DOM对应的Fiber实例return_targetInst,完整执行合成事件、遍历Fiber、执行用户回调 clearIfContinuousEvent:清空该事件类型的连续事件缓存队列,直接退出
- 调用核心事件执行函数
- 存在阻塞节点,优先处理高频连续事件:事件入队延迟执行,停止冒泡,放弃本次分发,不阻塞主线程
queueIfContinuousEvent判断当前是否为scroll/mousemove/touchmove等连续高频事件- 是连续事件:将本次事件存入延迟队列,等待水合完成后统一批量执行;调用
nativeEvent.stopPropagation停止原生事件冒泡,不继续向下分发,直接退出 - 非连续事件:执行
clearIfContinuousEvent清空历史缓存
- 是连续事件:将本次事件存入延迟队列,等待水合完成后统一批量执行;调用
- 捕获阶段 + 存在阻塞需要强制水合的离散交互事件(
click/input等):同步强制水合所有阻塞组件,水合完成后执行事件,若水合失败则停止冒泡,保证即时响应- 根据阻塞
DOM拿到对应Fiber attemptSynchronousHydration:同步阻塞主线程,强制完成该组件水合- 重新查找阻塞节点
nextBlockedOn - 如果水合后无阻塞节点,直接执行完整事件分发
- 如果前后阻塞节点不变,代表无法完成水合,跳出循环
- 更新
blockedOn,循环处理下一个阻塞DOM - 循环结束后,若仍存在未水合节点,停止原生事件冒泡,直接退出
- 根据阻塞
- 兜底分支:普通冒泡离散事件,存在阻塞但无需同步水合:不阻塞主线程,直接执行事件,动态查找
Fiber兼容半水合状态
设计意义
SSR/SSG页面:HTML先渲染到页面,JS加载完成后才会执行水合;此时点击DOM,对应组件还未挂载Fiber,无法读取事件回调,需要特殊兼容- 丢弃过期高频过渡事件:悬浮、焦点、拖拽类事件触发频率极高,水合阶段积压大量事件会造成水合完成后主线程卡顿;执行有效事件后清空缓存,减少后续批量执行开销,提升
SSR页面交互流畅度 - 高频滚动类事件不需要同步阻塞等待水合,入队延迟执行,避免主线程卡顿;同时停止冒泡,防止原生事件继续扩散
- 捕获阶段的用户点击、输入是强即时交互,不能延迟;因此强制同步水合组件,保证事件回调可以正常执行,解决
SSR页面点击失效问题 - 普通冒泡离散事件不阻塞主线程同步水合,直接尝试执行事件;框架内部动态兼容半水合状态,平衡交互响应速度与主线程阻塞开销
查找未水合阻塞 DOM:findInstanceBlockingEvent
- 专门识别三类会导致事件失效的特殊容器:脱水根、Suspense、Activity
- 判定是否存在水合阻塞容器,存在则返回阻塞 DOM;
- 缓存事件目标对应的有效 Fiber 到全局变量,供事件插件系统复用,避免重复查找 DOM-Fiber 映射
- 当事件触发节点自身未挂载,仅上层祖先挂载时,清空 Fiber 实例,避免事件系统读取未就绪组件的回调,防止报错
export function findInstanceBlockingEvent(
nativeEvent,
) {
const nativeEventTarget = getEventTarget(nativeEvent);
return findInstanceBlockingTarget(nativeEventTarget);
}
function getEventTarget(nativeEvent) {
let target = nativeEvent.target || nativeEvent.srcElement || window;
if (target.correspondingUseElement) {
target = target.correspondingUseElement;
}
return target.nodeType === TEXT_NODE ? target.parentNode : target;
}
export let return_targetInst = null;
export function findInstanceBlockingTarget(
targetNode,
) {
return_targetInst = null;
// 从 DOM 向上找最近绑定 Fiber 的实例
let targetInst = getClosestInstanceFromNode(targetNode);
if (targetInst !== null) {
// 从当前 Fiber 向上找最近已挂载的 Fiber
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
targetInst = null;
} else {
const tag = nearestMounted.tag;
// Suspense 组件,子树延迟加载,内部 DOM 已渲染但未激活
if (tag === SuspenseComponent) {
const instance = getSuspenseInstanceFromFiber(nearestMounted);
if (instance !== null) {
return instance;
}
targetInst = null;
// 离屏缓存组件(React 内部),DOM 存在但组件未激活
} else if (tag === ActivityComponent) {
const instance = getActivityInstanceFromFiber(nearestMounted);
if (instance !== null) {
return instance;
}
targetInst = null;
// 应用根 Fiber,对应 #root 容器
} else if (tag === HostRoot) {
const root = nearestMounted.stateNode;
if (isRootDehydrated(root)) {
return getContainerFromFiber(nearestMounted);
}
targetInst = null;
} else if (nearestMounted !== targetInst) {
targetInst = null;
}
}
}
return_targetInst = targetInst;
return null;
}
作用
- 获取事件真实触发
DOM:统一抹平浏览器事件target相关API差异,正确取出事件真正触发的DOM节点,拿到事件源头DOM,作为向上遍历查找阻塞节点的起点 - 转发给核心查找函数
findInstanceBlockingTarget- 调用
getClosestInstanceFromNode获取当前DOM绑定的最近Fiber:从传入的DOM节点向上遍历,拿到绑定在DOM上的Fiber实例 - 存在
Fiber实例,调用getNearestMountedFiber,从当前Fiber向上遍历Fiber树,找到第一个已完成挂载的祖先Fiber - 无任何已挂载祖先
Fiber:整条组件树都未挂载,不存在可响应事件的Fiber,清空targetInst,后续直接返回null - 最近挂载节点是
SuspensegetSuspenseInstanceFromFiber获取Suspense对应的DOM容器- 容器存在 → 直接返回该
DOM,代表被Suspense阻塞水合 - 无容器则清空
targetInst,视为无阻塞
- 最近挂载节点是
Activity离屏组件:DOM保留在页面但组件处于冻结 / 未激活状态,事件无法正常响应,找到对应DOM容器直接返回,标记为阻塞节点 - 最近挂载节点是应用根
HostRoot,判断根容器是否处于脱水未水合状态(SSR刚加载完HTML,JS还没完成水合)- 脱水状态:返回根容器
DOM,代表全局水合阻塞 - 已完成水合:清空
targetInst
- 脱水状态:返回根容器
- 最近挂载
Fiber不是当前事件节点自身:当前触发事件的Fiber自身未挂载,仅上层祖先挂载;组件未就绪,不能作为有效事件目标,清空targetInst - 把有效
Fiber存入全局变量供上层使用:走完所有阻塞判断后,若targetInst不为null,代表当前DOM对应的组件已正常挂载、无阻塞;存入全局变量,后续插件事件系统可以直接拿到事件目标Fiber,快速收集事件处理器 - 函数统一返回
null:只有上面Suspense/Activity/ 脱水根三个分支会提前return DOM阻塞节点;其余所有正常、无阻塞场景统一返回null
- 调用
清理缓存的过渡类事件:clearIfContinuousEvent
清理 SSR 半水合阶段积压的过渡类事件缓存,最终只执行最新一次交互事件,中间滑动产生的过渡事件全部作废。当页面处于半水合状态时,queueIfContinuousEvent 会把频繁触发的悬浮、焦点、拖拽、指针事件存入全局队列延迟执行;一旦当前事件可以正常分发(组件已水合、无阻塞),调用本函数清空对应缓存,避免过期事件后续补发造成状态错乱
- 本次焦点事件正常分发执行,清空之前缓存的焦点队列,丢弃未执行的旧焦点事件
- 清空拖拽事件缓存,过期拖拽事件不再补发
- 清空鼠标悬浮缓存
- 只删除当前触点对应的缓存事件,不影响其他触点队列(指针设备支持多触点,用
pointerId区分不同手指 / 触控笔) - 按触点
ID清除对应指针捕获缓存事件
export function clearIfContinuousEvent(
domEventName,
nativeEvent,
) {
switch (domEventName) {
case 'focusin':
case 'focusout':
// 缓存 focusin /focusout 焦点过渡事件
queuedFocus = null;
break;
case 'dragenter':
case 'dragleave':
// 缓存 dragenter /dragleave 拖拽进出事件
queuedDrag = null;
break;
case 'mouseover':
case 'mouseout':
// 缓存 mouseover /mouseout 鼠标悬浮进出事件
queuedMouse = null;
break;
case 'pointerover':
case 'pointerout': {
// 缓存 pointerover/pointerout 指针悬浮事件
const pointerId = nativeEvent.pointerId;
queuedPointers.delete(pointerId);
break;
}
case 'gotpointercapture':
case 'lostpointercapture': {
// 缓存 gotpointercapture/lostpointercapture 指针捕获事件
const pointerId = nativeEvent.pointerId;
queuedPointerCaptures.delete(pointerId);
break;
}
}
}
缓存高频过渡类事件:queueIfContinuousEvent
focusin / dragenter / mouseover / pointerover / gotpointercapture 均为高频、成对切换、瞬时状态过渡事件,短时间会大量连续触发;SSR 半水合阶段如果立刻执行会阻塞主线程,且大量中间事件无业务价值,只需要保留最新一次回放
- 匹配到可缓存的连续过渡事件:存入缓存并返回
true;上层执行nativeEvent.stopPropagation(),终止本次事件分发 - 不匹配任何
case(click/input/scroll/wheel等):返回false,上层继续走正常阻塞事件分发逻辑
export function queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
) {
switch (domEventName) {
case 'focusin': {
const focusEvent = nativeEvent;
queuedFocus = accumulateOrCreateContinuousQueuedReplayableEvent(
queuedFocus,
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
focusEvent,
);
return true;
}
case 'dragenter': {
const dragEvent = nativeEvent;
queuedDrag = accumulateOrCreateContinuousQueuedReplayableEvent(
queuedDrag,
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
dragEvent,
);
return true;
}
case 'mouseover': {
const mouseEvent = nativeEvent;
queuedMouse = accumulateOrCreateContinuousQueuedReplayableEvent(
queuedMouse,
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
mouseEvent,
);
return true;
}
case 'pointerover': {
const pointerEvent = nativeEvent;
const pointerId = pointerEvent.pointerId;
queuedPointers.set(
pointerId,
accumulateOrCreateContinuousQueuedReplayableEvent(
queuedPointers.get(pointerId) || null,
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
pointerEvent,
),
);
return true;
}
case 'gotpointercapture': {
const pointerEvent = nativeEvent;
const pointerId = pointerEvent.pointerId;
queuedPointerCaptures.set(
pointerId,
accumulateOrCreateContinuousQueuedReplayableEvent(
queuedPointerCaptures.get(pointerId) || null,
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
pointerEvent,
),
);
return true;
}
}
return false;
}
连续过渡事件缓存工具:accumulateOrCreateContinuousQueuedReplayableEvent
- 无旧缓存 / 原生事件对象不同:调用
createQueuedReplayableEvent新建一份可回放事件快照,统一封装回放所需全部上下文,后续水合完成后遍历缓存、执行事件分发时,仅依赖QueuedReplayableEvent即可完整复现当时事件环境,无需额外保存临时变量,并对阻塞Fiber提升水合优先级,调度器会优先执行这个组件的水合逻辑,尽快解除阻塞,减少事件持续缓存积压,避免大量过渡事件长期驻留内存- 保存阻塞节点、事件名称、初始
flags targetContainers数组存入当前根容器- 绑定原生事件对象作为唯一标识
- 旧缓存会被本次新快照覆盖,旧快照失去引用,等待
GC回收,只保留最新一次交互状态 - 从阻塞
DOM取出对应Fiber - 标记该
Fiber为高优连续水合任务 - 返回新建快照,覆盖全局缓存
- 保存阻塞节点、事件名称、初始
- 已经存在同一份原生事件缓存:合并标记、收集多根容器,复用旧快照(高频过渡事件短时间大量触发,如果每次都新建快照会频繁
GC),不重复创建,减少内存对象创建,降低GC开销- 把本次监听的捕获 / 非委托标记合并到旧快照,回放时同时携带两套标记
- 快照内
targetContainers是数组,存储所有触发该事件的根容器;如果当前根容器不在数组中,追加进去;保证回放时能在所有关联root分发事件 - 返回原有合并后的快照
function accumulateOrCreateContinuousQueuedReplayableEvent(
existingQueuedEvent,
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
) {
if (
existingQueuedEvent === null ||
existingQueuedEvent.nativeEvent !== nativeEvent
) {
const queuedEvent = createQueuedReplayableEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
if (blockedOn !== null) {
const fiber = getInstanceFromNode(blockedOn);
if (fiber !== null) {
// 给被阻塞的 Fiber 打上连续事件水合优先级,调度器优先处理该组件水合,尽快解除阻塞
attemptContinuousHydration(fiber);
}
}
return queuedEvent;
}
existingQueuedEvent.eventSystemFlags |= eventSystemFlags;
const targetContainers = existingQueuedEvent.targetContainers;
if (
targetContainer !== null &&
targetContainers.indexOf(targetContainer) === -1
) {
targetContainers.push(targetContainer);
}
return existingQueuedEvent;
}
function createQueuedReplayableEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
) {
return {
blockedOn,
domEventName,
eventSystemFlags,
nativeEvent,
targetContainers: [targetContainer],
};
}
事件边界校正器 + 批量更新入口:dispatchEventForPluginEventSystem
- 对常规委托事件,修正事件向上遍历的顶层边界
ancestorInst,保证事件冒泡仅在当前React根容器内部传播,不跨root、不跨Portal污染其他应用- 单根应用普通
DOM:向上遍历Fiber命中当前HostRoot,break循环,ancestorInst停在根Fiber,事件冒泡范围整个root内部 Portal内部触发事件Portal容器不属于当前root,内层循环向上查找父Fiber- 若
Portal挂载到外部DOM,直接return,事件不会冒泡到Portal外部组件 - 若
Portal容器DOM能找到内部托管组件,重置起点重新查找根边界
- 多
Root并行应用:每个root独立绑定事件,边界校正会限制冒泡仅在当前root内,A根事件不会触发B根组件回调,隔离多应用
- 单根应用普通
- 把插件事件执行逻辑包裹进批量更新,统一触发批量渲染
export function dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
) {
let ancestorInst = targetInst;
if (
// 当前 DOM 是 React 托管组件,不是第三方外部 DOM
(eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
// 当前事件支持冒泡委托(click/input 等常规事件,不是 scroll/focus 这类不冒泡事件)
(eventSystemFlags & IS_NON_DELEGATED) === 0
) {
const targetContainerNode = targetContainer;
if (targetInst !== null) {
let node = targetInst;
mainLoop: while (true) {
if (node === null) {
return;
}
const nodeTag = node.tag;
if (nodeTag === HostRoot || nodeTag === HostPortal) {
let container = node.stateNode.containerInfo;
// 判断 DOM 容器是否为当前事件绑定的根容器
if (isMatchingRootContainer(container, targetContainerNode)) {
break;
}
if (nodeTag === HostPortal) {
let grandNode = node.return;
while (grandNode !== null) {
const grandTag = grandNode.tag;
if (grandTag === HostRoot || grandTag === HostPortal) {
const grandContainer = grandNode.stateNode.containerInfo;
if (
isMatchingRootContainer(grandContainer, targetContainerNode)
) {
return;
}
}
grandNode = grandNode.return;
}
}
while (container !== null) {
const parentNode = getClosestInstanceFromNode(container);
if (parentNode === null) {
return;
}
const parentTag = parentNode.tag;
if (
parentTag === HostComponent ||
parentTag === HostText ||
parentTag === HostHoistable ||
parentTag === HostSingleton
) {
node = ancestorInst = parentNode;
continue mainLoop;
}
container = container.parentNode;
}
}
node = node.return;
}
}
}
// 批量更新,内部回调所有 setState 会合并批量渲染,减少重渲染
batchedUpdates(() =>
// 事件插件体系真正执行逻辑,生成合成事件、遍历 Fiber 执行 onXxx 回调
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer,
),
);
}
作用
- 初始化事件向上冒泡遍历
Fiber树的终止边界节点ancestorInst - 仅对托管 + 可委托事件做边界裁剪:当前
DOM是React托管组件,不是第三方外部DOM且当前事件支持冒泡委托(click/input等常规事件,不是scroll/focus这类不冒泡事件) - 仅目标存在
Fiber才执行边界遍历 - 向上遍历
Fiber树,寻找当前根容器边界:从当前Fiber持续向上node.return遍历父Fiber - 遍历到
HostRoot / HostPortal(根 / 传送门)- 取出
Root/Portal对应的真实DOM容器 - 如果容器等于当前事件绑定的根容器 → 命中边界,跳出循环,当前
node作为冒泡终点 - 不匹配则进入
Portal特殊处理逻辑
- 取出
Portal特殊处理:跨Portal不能冒泡到外部根,Portal节点渲染到外部DOM,逻辑上父级不在当前root内部,隔离传送门事件作用域- 向上查找
Portal的父Fiber链 - 若找到当前根容器,说明
Portal渲染范围在外部,直接return终止整个函数
- 向上查找
- 向上回溯
DOM容器,重新定位容器内Fiber(Portal跨DOM场景修复):Portal渲染到外部DOM,Fiber树和DOM树层级不一致- 从
Portal容器DOM向上遍历原生DOM - 找到第一个
React托管DOM组件Fiber - 重置遍历起点
node和冒泡边界ancestorInst,重启主循环重新查找根边界 - 找不到托管
Fiber直接return,终止事件分发
- 从
- 未命中
Root/Portal,继续向上遍历父Fiber - 边界修正完成,进入批量执行完整合成事件流程
设计意义
- 多根、
Portal事件隔离,修复原生DOM冒泡穿透问题:浏览器原生DOM冒泡不受React根容器、Portal边界限制,会跨DOM树传播;本函数通过校正ancestorInst限定事件遍历上限,实现多#root应用事件互相隔离和Portal内部事件不冒泡到外部父组件。处理Portal渲染到外部DOM导致Fiber、DOM层级不匹配的问题,抹平DOM树与Fiber树层级不一致的行为差异 - 统一批量更新包装:所有用户事件回调统一包裹在
batchedUpdates,自动合并多次状态更新,避免高频事件重复渲染,保障页面性能
事件收集与回调执行:dispatchEventsForPlugins
- 调用插件系统,从
Fiber树提取所有捕获 / 冒泡事件处理器,存入调度队列 - 按阶段顺序执行队列里的所有事件回调
function dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
) {
const nativeEventTarget = getEventTarget(nativeEvent);
// 事件调度队列,数组结构,存储每条事件处理记录(包含阶段、回调、Fiber 实例、合成事件配置等)
const dispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
// 按捕获先、冒泡后的顺序遍历队列,创建 SyntheticEvent 合成事件、执行用户回调、处理事件池、阻止冒泡 / 默认行为
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
作用
- 调用
getEventTarget获取标准化原生事件目标DOM - 初始化空事件调度队列
dispatchQueue:存放所有待执行的事件处理器。每条队列项会记录事件阶段(捕获 / 冒泡)、对应Fiber、事件回调、插件配置等信息 - 调用
extractEvents插件体系提取所有事件处理器,填充队列,执行完毕后dispatchQueue已有序存放全部待执行回调- 遍历
React内置所有事件插件(点击插件、表单插件、焦点插件、触摸插件等) - 从
targetInst向上遍历Fiber树,直到上层限定的边界ancestorInst - 区分捕获阶段回调 (
onXxxCapture) 、冒泡阶段回调 (onXxx) ,分别推入队列 - 插件负责映射原生事件与
React合成事件、处理特殊事件(如onChange、onSelect)
- 遍历
- 调用
processDispatchQueue按阶段顺序执行队列内所有事件- 区分捕获 / 冒泡顺序:先执行全部捕获回调,再执行全部冒泡回调,模拟
DOM原生事件传播模型 - 复用事件池,创建
SyntheticEvent合成事件对象,挂载原生事件、stopPropagation/preventDefault封装方法 - 依次执行用户写的事件回调函数
- 监听回调中调用
e.stopPropagation(),截断后续同阶段事件执行 - 回调执行完毕回收合成事件对象,放回事件池复用,减少
GC开销
- 区分捕获 / 冒泡顺序:先执行全部捕获回调,再执行全部冒泡回调,模拟
设计思想
- 优先级包装器策略:
createEventListenerWrapperWithPriority根据事件类型选择dispatchDiscreteEvent/dispatchContinuousEvent/dispatchEvent,在原生事件触发时同步设置currentUpdatePriority,决定后续setState的lane - 被动事件仿 document 行为:
Chrome对document上的touchstart/touchmove/wheel强制{ passive: true },React 19在rootContainer上手动模拟(非被动时浏览器阻塞滚动等待JS) - 偏函数闭包缓存:
bind(null, domEventName, eventSystemFlags, targetContainer)创建一次性闭包,原生addEventListener的listener引用在整个root生命周期中不变,无需重新绑定