React 事件系统addTrappedEventListener的使用
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 清空历史缓存
- queueIfContinuousEvent 判断当前是否为 scroll/mousemove/touchmove 等连续高频事件
- 捕获阶段 + 存在阻塞需要强制水合的离散交互事件(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
- 最近挂载节点是 Suspense
- getSuspenseInstanceFromFiber 获取 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 生命周期中不变,无需重新绑定
到此这篇关于React 事件系统addTrappedEventListener的使用的文章就介绍到这了,更多相关React addTrappedEventListener内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
详解开发react应用最好用的脚手架 create-react-app
本篇文章主要介绍了详解开发react应用最好用的脚手架 create-react-app,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧2018-04-04
React使用xlsx和js-export-excel实现前端导出
这篇文章主要为大家详细介绍了React如何分别使用xlsx和js-export-excel实现前端导出功能,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下2024-02-02
react antd-mobile ActionSheet+tag实现多选方式
这篇文章主要介绍了react antd-mobile ActionSheet+tag实现多选方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教2023-10-10
React Native自定义Android的SSL证书链校验
这篇文章主要为大家介绍了React Native自定义Android的SSL证书链校验示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2022-10-10
关于react ant 组件 Select下拉框 值回显的问题
这篇文章主要介绍了关于react ant 组件 Select下拉框 值回显的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2022-08-08


最新评论