React 事件系统:addTrappedEventListener

React 合成事件系统最底层 DOM 监听器创建与绑定核心函数

  • 生成 React 封装的高优先级事件回调
  • 自动区分 touch/wheel 事件开启 passive 优化
  • 根据捕获 / 冒泡 + 是否 passive 分流调用对应底层绑定 API
  • 生成解绑句柄,供一次性监听自动销毁

完整调用链路全览

scss 复制代码
──────────────────────────────────────────────────────────────────────
 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 内部渲染调度、合成事件机制解耦,所有底层监听统一使用标准化包装函数,上层不用关心调度细节

javascript 复制代码
let listener = createEventListenerWrapperWithPriority(
  targetContainer,
  domEventName,
  eventSystemFlags,
);

步骤 ②:被动事件优化

浏览器支持 passive 配置时,滚动 / 触摸类事件强制设为 passive: true。自动做滚动性能优化,开发者无需手动配置,抹平浏览器滚动卡顿缺陷

javascript 复制代码
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.passivegetter 会被触发,passiveBrowserEventsSupported 设为 true。如果浏览器不支持此选项(如 IE ≤ 11),catch 块设为 false

设计意义

Chrome 56+touchstart/touchmove/wheeldocument 级监听器默认设为 { passive: true }(因为开发者几乎不会在 touch 事件上调用 preventDefault 但必须等待 JS 执行判断是否调用)。React 19 不把事件绑到 document 而绑到 rootContainer,手动模拟这个 passive 行为以获得相同性能收益


步骤 ③:选择 addEventListener 重载

根据捕获 / 冒泡 + 是否 passive 分流到 4 个底层绑定工具,统一管理 DOM addEventListener 参数差异,保存解绑函数,支撑一次性监听自动移除,为后续全局卸载根容器事件提供基础能力

javascript 复制代码
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;
}

四种组合

  • 捕获 + passivetouch/wheel
  • 捕获 + 无 passive(普通事件如 clickCapture
  • 冒泡 + passivetouch/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

javascript 复制代码
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 读取信息
    • 预固化三个前置参数:domEventNameeventSystemFlagstargetContainer
    • 当浏览器触发事件执行回调时,原生 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 环境,多个事件嵌套、并发执行时上下文互不干扰
javascript 复制代码
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 标记,滚动相关更新不会被当作延迟渲染任务,避免滚动滞后、画面掉帧
  • 安全隔离全局调度环境,无副作用污染 :采用缓存现场 → 临时覆盖 → 最终强制还原的栈式上下文模式,单次事件执行仅局部生效,函数退出后全局状态完全复原,异常场景也不会破坏全局调度规则
javascript 复制代码
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:直接执行完整事件流程
  • 高频连续事件遇阻塞:暂存队列、丢弃本次触发
  • 捕获阶段离散交互事件遇阻塞:同步强制水合组件,再执行事件
  • 其余普通阻塞场景:直接执行事件(不等待水合)
javascript 复制代码
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 页面点击失效问题
  • 普通冒泡离散事件不阻塞主线程同步水合,直接尝试执行事件;框架内部动态兼容半水合状态,平衡交互响应速度与主线程阻塞开销

查找未水合阻塞 DOM:findInstanceBlockingEvent

  • 专门识别三类会导致事件失效的特殊容器:脱水根、Suspense、Activity
  • 判定是否存在水合阻塞容器,存在则返回阻塞 DOM;
  • 缓存事件目标对应的有效 Fiber 到全局变量,供事件插件系统复用,避免重复查找 DOM-Fiber 映射
  • 当事件触发节点自身未挂载,仅上层祖先挂载时,清空 Fiber 实例,避免事件系统读取未就绪组件的回调,防止报错
javascript 复制代码
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 刚加载完 HTMLJS 还没完成水合)
      • 脱水状态:返回根容器 DOM,代表全局水合阻塞
      • 已完成水合:清空 targetInst
    • 最近挂载 Fiber 不是当前事件节点自身:当前触发事件的 Fiber 自身未挂载,仅上层祖先挂载;组件未就绪,不能作为有效事件目标,清空 targetInst
    • 把有效 Fiber 存入全局变量供上层使用:走完所有阻塞判断后,若 targetInst 不为 null,代表当前 DOM 对应的组件已正常挂载、无阻塞;存入全局变量,后续插件事件系统可以直接拿到事件目标 Fiber,快速收集事件处理器
    • 函数统一返回 null:只有上面 Suspense/Activity/ 脱水根三个分支会提前 return DOM 阻塞节点;其余所有正常、无阻塞场景统一返回 null

事件边界校正器 + 批量更新入口:dispatchEventForPluginEventSystem

  • 对常规委托事件,修正事件向上遍历的顶层边界 ancestorInst,保证事件冒泡仅在当前 React 根容器内部 传播,不跨 root、不跨 Portal 污染其他应用
    • 单根应用普通 DOM :向上遍历 Fiber 命中当前 HostRootbreak 循环,ancestorInst 停在根 Fiber,事件冒泡范围整个 root 内部
    • Portal 内部触发事件
      • Portal 容器不属于当前 root,内层循环向上查找父 Fiber
      • Portal 挂载到外部 DOM,直接 return,事件不会冒泡到 Portal 外部组件
      • Portal 容器 DOM 能找到内部托管组件,重置起点重新查找根边界
    • Root 并行应用 :每个 root 独立绑定事件,边界校正会限制冒泡仅在当前 root 内,A 根事件不会触发 B 根组件回调,隔离多应用
  • 把插件事件执行逻辑包裹进批量更新,统一触发批量渲染
javascript 复制代码
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
  • 仅对托管 + 可委托事件 做边界裁剪:当前 DOMReact 托管组件,不是第三方外部 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 容器,重新定位容器内 FiberPortalDOM 场景修复):Portal 渲染到外部 DOMFiber 树和 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 导致 FiberDOM 层级不匹配的问题,抹平 DOM 树与 Fiber 树层级不一致的行为差异
  • 统一批量更新包装 :所有用户事件回调统一包裹在 batchedUpdates,自动合并多次状态更新,避免高频事件重复渲染,保障页面性能

事件收集与回调执行:dispatchEventsForPlugins

  • 调用插件系统,从 Fiber 树提取所有捕获 / 冒泡事件处理器,存入调度队列
  • 按阶段顺序执行队列里的所有事件回调
javascript 复制代码
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 合成事件、处理特殊事件(如 onChangeonSelect
  • 调用 processDispatchQueue 按阶段顺序执行队列内所有事件
    • 区分捕获 / 冒泡顺序:先执行全部捕获回调,再执行全部冒泡回调 ,模拟 DOM 原生事件传播模型
    • 复用事件池,创建 SyntheticEvent 合成事件对象,挂载原生事件、stopPropagation/preventDefault 封装方法
    • 依次执行用户写的事件回调函数
    • 监听回调中调用 e.stopPropagation(),截断后续同阶段事件执行
    • 回调执行完毕回收合成事件对象,放回事件池复用,减少 GC 开销

设计思想

  • 优先级包装器策略createEventListenerWrapperWithPriority 根据事件类型选择 dispatchDiscreteEvent/dispatchContinuousEvent/dispatchEvent,在原生事件触发时同步设置 currentUpdatePriority,决定后续 setStatelane
  • 被动事件仿 document 行为Chromedocument 上的 touchstart/touchmove/wheel 强制 { passive: true }React 19rootContainer 上手动模拟(非被动时浏览器阻塞滚动等待 JS
  • 偏函数闭包缓存bind(null, domEventName, eventSystemFlags, targetContainer) 创建一次性闭包,原生 addEventListenerlistener 引用在整个 root 生命周期中不变,无需重新绑定
相关推荐
小妖6662 小时前
React input 输入卡顿问题
前端·javascript·react.js
小二·16 小时前
React 19 + Next.js 15 现代前端开发实战:App Router / Server Components / 流式渲染
前端·javascript·react.js
YFF菲菲兔1 天前
React 事件系统:listenToAllSupportedEvents
react.js
光影少年1 天前
RN 渲染原理:不依赖WebView,原生控件渲染
前端·react native·react.js·前端框架
LaughingZhu2 天前
智能体经典范式构建:ReAct、Plan-and-Solve 与 Reflection
前端·react.js·前端框架
飞天狗2 天前
长列表滚动卡顿怎么破:虚拟列表选型 + react-virtuoso 实战调优指南
react.js·性能优化
binbin_522 天前
React 井字棋教程:用一个小游戏理解组件和状态
前端·react.js·前端框架
张元清2 天前
React useObjectUrl Hook:预览文件与 Blob,不留内存泄漏(2026)
javascript·react.js
我是大卫2 天前
【图】React源码解析-第五部分:底层数据结构与内存模型
react.js·源码阅读