React 事件系统:listenToAllSupportedEvents

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 上挂载唯一标记,标记当前容器已完成全部事件绑定,后续再次调用函数会直接跳过绑定逻辑
  • 已存在标记 → 直接跳过,避免重复执行绑定

设计意义

整个事件监听初始化是一次性的 :所有事件的 listenerrootContainer 创建时注册一次,之后 fibermount/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 将事件委托到 rootContainercreateRoot 时一次性绑定所有事件,之后 fiber 的增删改不需要操作原生 addEventListener,只需要在 fiber 上记录 updateQueue,事件触发时再通过 getListenerfiber 树中查找
  • allNativeEventsReact 内部维护的所有支持的原生 DOM 事件名全集clickmouseoverinputchange 等)
  • nonDelegatedEventsSet 集合,存储不支持事件委托、必须直接绑定到目标元素 的事件(如 scrollfocusblur 等,这类事件不冒泡,无法通过根容器代理)
  • React 的合成事件模拟了捕获 + 冒泡两阶段:
    • capture 阶段:从根到目标(root → ... → target
    • bubble 阶段:从目标到根(target → ... → root
  • React 通过 capture listener 实现 capture 阶段模拟:
    • ReactrootContainer 上注册 capture listener
    • 收到事件后,从 target 向上遍历 fiber 树构建 listener 列表
    • 按捕获顺序(逆序)执行
  • React 通过 bubble listener 实现 bubble 阶段模拟:
    • ReactrootContainer 上注册 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 对象
    • 如果根容器本身就是 documentSSR / 特殊挂载场景),直接赋值自身;
    • 普通 #root 容器,取其 ownerDocument 获取页面文档对象
  • document 判空保护:兼容 SSR、脱离文档流的 DOM 节点,防止 ownerDocumentnull 时执行绑定报错,做空值安全防护
  • document 重复绑定拦截(同根容器标记逻辑):给 document 也挂载独立标记,防止重复绑定 selectionchange,多根实例复用同一个 document 监听
  • 绑定特殊事件 selectionchange:在 document 上绑定冒泡阶段的 selectionchange 监听器

设计意义

selectionchangeReact 事件系统中唯一一个不在 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,此后 fibermount/update/unmount 不需要操作 addEventListener,通过 __reactProps$fiber 上获取当前 handler
  • capture + bubble 双监听 :每个事件绑定两个监听器(非委托事件只绑 capture),React 通过从 targetrootfiber 树遍历 + listener 列表正序/逆序执行模拟完整的两阶段事件流
  • 被动事件优化touchstart / touchmove / wheel 使用 { passive: true },浏览器承诺不调用 preventDefault(),允许合成层在事件处理完成前就开始滚动,消除 touchstart 导致的滚动延迟
  • 非委托事件单独处理scroll, load, invalid, toggle 等不冒泡或性能敏感事件只在 capture 阶段监听,避免冒泡到 root 的空遍历
  • selectionchange 特殊路径 :唯一不在 rootContainer 上监听的事件------它不在元素上触发,在 Document 上触发;使用独立的 listeningMarker 防重
  • 随机标记防冲突_reactListening + 随机后缀避免与第三方库在 DOM 上的属性碰撞
  • 惰性提取 + 批量 dispatchlistenToAllSupportedEvents 只绑定 listener,不关注 handlerhandler 通过 accumulateSinglePhaseListeners 在事件触发时fiber 收集,绑定时不知道具体有哪些 onClick
相关推荐
小二·21 小时前
React 19 + Next.js 15 现代前端开发实战:App Router / Server Components / 流式渲染
前端·javascript·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·源码阅读
真的想不出名儿2 天前
Md编辑器整合-react
前端·react.js·编辑器
牛仔只喝牛奶3 天前
多端跨端技术选型:小程序 + H5 + App 的两栈拆分落地
react native·react.js