Skip to content

第13章 事件系统

本章将深入React的事件系统,理解合成事件的设计理念、事件委托机制、事件注册与触发流程,以及事件优先级如何与调度系统集成。

在前端开发中,事件处理是最常见的交互方式。用户点击按钮、输入文字、滚动页面,这些都会触发相应的事件。React并没有直接使用浏览器原生事件,而是实现了一套自己的事件系统——合成事件(Synthetic Events)。

为什么React要重新实现事件系统?合成事件与原生事件有什么区别?React如何通过事件委托优化性能?事件优先级如何影响更新调度?

本章将逐一解答这些问题,带你深入理解React事件系统的设计与实现。


13.1 React事件系统概述

React事件系统是React与用户交互的桥梁,它在浏览器原生事件之上构建了一层抽象。

13.1.1 为什么需要合成事件

在React出现之前,开发者直接使用浏览器原生事件。但原生事件存在一些问题:

1. 浏览器兼容性问题

不同浏览器的事件实现存在差异:

  • IE使用attachEvent,其他浏览器使用addEventListener
  • 事件对象的属性名不一致(如event.target vs event.srcElement
  • 阻止默认行为的方法不同(preventDefault() vs returnValue = false

2. 性能问题

如果为每个DOM节点都绑定事件监听器,会消耗大量内存:

javascript
// 传统方式:为1000个按钮绑定事件
for (let i = 0; i < 1000; i++) {
  document.getElementById(`btn-${i}`)
    .addEventListener('click', handleClick);
}
// 需要创建1000个事件监听器

3. 事件处理的一致性

React需要在不同平台(Web、Native)上提供一致的事件API。

React的解决方案:合成事件

React实现了一套合成事件系统,解决了上述问题:

javascript
// React合成事件
function MyButton() {
  const handleClick = (e) => {
    // e是React的合成事件对象(SyntheticEvent)
    console.log(e.type);        // "click"
    console.log(e.target);      // 目标DOM节点
    console.log(e.nativeEvent); // 原生事件对象
    
    e.preventDefault();  // 统一的API
    e.stopPropagation(); // 统一的API
  };
  
  return <button onClick={handleClick}>点击我</button>;
}

合成事件的优势

  1. 跨浏览器兼容:统一的事件对象接口
  2. 性能优化:通过事件委托减少内存占用
  3. 与React更新机制集成:支持事件优先级
  4. 事件池优化:复用事件对象(React 17之前)

13.1.2 事件委托机制

React使用事件委托(Event Delegation)来优化性能。

什么是事件委托?

事件委托利用了事件冒泡机制:不在每个子元素上绑定事件,而是在父元素上统一监听。

React的事件委托实现

在React 17之前,所有事件都委托到document上:

javascript
// React 16及之前
document.addEventListener('click', dispatchEvent);
document.addEventListener('change', dispatchEvent);
// ...

从React 17开始,事件委托到React根容器上:

javascript
// React 17+
const rootContainer = document.getElementById('root');
rootContainer.addEventListener('click', dispatchEvent);
rootContainer.addEventListener('change', dispatchEvent);
// ...

为什么改为委托到根容器?

  1. 支持多个React应用共存:避免不同React版本冲突
  2. 更好的事件隔离:不同根容器的事件互不干扰
  3. 渐进式迁移:方便从旧版本升级

事件委托的工作流程

示例:事件委托的内存优势

javascript
// 传统方式:1000个按钮 = 1000个监听器
function TraditionalList() {
  const buttons = [];
  for (let i = 0; i < 1000; i++) {
    const btn = document.createElement('button');
    btn.addEventListener('click', () => {
      console.log(`Button ${i} clicked`);
    });
    buttons.push(btn);
  }
  return buttons;
}

// React方式:1000个按钮 = 1个监听器(在根容器上)
function ReactList() {
  const items = Array.from({ length: 1000 }, (_, i) => i);
  
  return (
    <div>
      {items.map(i => (
        <button key={i} onClick={() => console.log(`Button ${i} clicked`)}>
          Button {i}
        </button>
      ))}
    </div>
  );
}

在React中,无论有多少个按钮,根容器上只有一个click事件监听器。

13.1.3 合成事件对象

React的合成事件对象(SyntheticEvent)是对原生事件的封装。

SyntheticEvent的结构

javascript
// 文件:packages/react-dom-bindings/src/events/SyntheticEvent.js
// 行号:50-100(React 19.3.0)

function SyntheticEvent(
  dispatchConfig,
  targetInst,
  nativeEvent,
  nativeEventTarget,
) {
  this.dispatchConfig = dispatchConfig;
  this._targetInst = targetInst;
  this.nativeEvent = nativeEvent;
  this._dispatchListeners = null;
  this._dispatchInstances = null;
  
  // 从原生事件复制属性
  const Interface = this.constructor.Interface;
  for (const propName in Interface) {
    const normalize = Interface[propName];
    if (normalize) {
      this[propName] = normalize(nativeEvent);
    } else {
      this[propName] = nativeEvent[propName];
    }
  }
  
  // 标准化target和currentTarget
  this.target = nativeEventTarget;
  this.currentTarget = null;
  
  return this;
}

合成事件的关键属性

属性说明示例
type事件类型"click", "change"
target触发事件的DOM节点<button>
currentTarget当前处理事件的DOM节点<div>
nativeEvent原生事件对象MouseEvent
bubbles是否冒泡true
cancelable是否可取消true
defaultPrevented是否已阻止默认行为false
eventPhase事件阶段3(冒泡阶段)
isTrusted是否由用户触发true
timeStamp事件时间戳1234567890

合成事件的方法

javascript
// 阻止默认行为
event.preventDefault();

// 阻止事件冒泡
event.stopPropagation();

// 立即停止事件传播(不执行后续监听器)
event.stopImmediatePropagation();

// 检查是否已阻止默认行为
if (event.defaultPrevented) {
  // ...
}

// 访问原生事件
const nativeEvent = event.nativeEvent;

示例:合成事件与原生事件的对比

javascript
function EventComparison() {
  const handleClick = (syntheticEvent) => {
    console.log('=== 合成事件 ===');
    console.log('类型:', syntheticEvent.type);
    console.log('目标:', syntheticEvent.target);
    console.log('时间戳:', syntheticEvent.timeStamp);
    
    console.log('=== 原生事件 ===');
    const nativeEvent = syntheticEvent.nativeEvent;
    console.log('类型:', nativeEvent.type);
    console.log('目标:', nativeEvent.target);
    console.log('时间戳:', nativeEvent.timeStamp);
    
    // 合成事件和原生事件的时间戳可能不同
    // 因为合成事件是在原生事件触发后创建的
  };
  
  return <button onClick={handleClick}>点击查看事件对象</button>;
}

React 17的重要变更:移除事件池

在React 16及之前,合成事件对象会被放入事件池中复用,以提高性能:

javascript
// React 16:事件对象会被复用
function handleClick(e) {
  console.log(e.type); // "click"
  
  setTimeout(() => {
    console.log(e.type); // null(事件对象已被清空)
  }, 100);
}

// 如果需要异步访问,必须调用persist()
function handleClick(e) {
  e.persist(); // 从事件池中移除,保留事件对象
  
  setTimeout(() => {
    console.log(e.type); // "click"(仍然可用)
  }, 100);
}

从React 17开始,移除了事件池机制:

javascript
// React 17+:事件对象不会被复用
function handleClick(e) {
  console.log(e.type); // "click"
  
  setTimeout(() => {
    console.log(e.type); // "click"(仍然可用)
  }, 100);
  
  // 不再需要e.persist()
}

为什么移除事件池?

  1. 现代浏览器性能提升:创建对象的成本已经很低
  2. 避免混淆:事件池机制容易导致bug
  3. 简化代码:不需要记住何时调用persist()

13.2 事件注册与触发

React事件系统的核心流程包括事件注册和事件触发两个阶段。

13.2.1 listenToAllSupportedEvents

React在应用启动时,会在根容器上注册所有支持的事件。

事件注册的时机

javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMRoot.js
// 行号:150-200(React 19.3.0)

export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions,
): RootType {
  // 创建FiberRoot
  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
  );
  
  // 标记容器
  markContainerAsRoot(root.current, container);
  
  // 注册所有支持的事件
  const rootContainerElement =
    container.nodeType === COMMENT_NODE
      ? container.parentNode
      : container;
  listenToAllSupportedEvents(rootContainerElement);
  
  return new ReactDOMRoot(root);
}

listenToAllSupportedEvents实现

javascript
// 文件:packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
// 行号:200-250(React 19.3.0)

export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  // 避免重复注册
  if (!(rootContainerElement: any)[listeningMarker]) {
    (rootContainerElement: any)[listeningMarker] = true;
    
    // 遍历所有支持的事件类型
    allNativeEvents.forEach((domEventName) => {
      // 某些事件不需要在根容器上监听
      if (domEventName !== 'selectionchange') {
        // 注册捕获阶段和冒泡阶段的监听器
        if (!nonDelegatedEvents.has(domEventName)) {
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }
        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });
    
    // 特殊处理selectionchange事件
    const ownerDocument =
      (rootContainerElement: any).nodeType === DOCUMENT_NODE
        ? rootContainerElement
        : (rootContainerElement: any).ownerDocument;
    if (ownerDocument !== null) {
      if (!(ownerDocument: any)[listeningMarker]) {
        (ownerDocument: any)[listeningMarker] = true;
        listenToNativeEvent('selectionchange', false, ownerDocument);
      }
    }
  }
}

支持的事件类型

React支持的原生事件存储在allNativeEvents集合中:

javascript
// 文件:packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
// 行号:100-150(React 19.3.0)

const allNativeEvents: Set<DOMEventName> = new Set();

// 注册简单事件
registerSimpleEvents();

// 注册其他事件插件
registerEvents();

// allNativeEvents包含:
// - 鼠标事件:click, mousedown, mouseup, mousemove, ...
// - 键盘事件:keydown, keyup, keypress
// - 表单事件:change, input, submit, reset
// - 焦点事件:focus, blur, focusin, focusout
// - 触摸事件:touchstart, touchmove, touchend, touchcancel
// - 滚动事件:scroll
// - 拖拽事件:drag, dragstart, dragend, drop, ...
// - 媒体事件:play, pause, ended, ...
// - 其他事件:load, error, resize, ...

listenToNativeEvent实现

javascript
// 文件:packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
// 行号:300-350(React 19.3.0)

export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  target: EventTarget,
): void {
  let eventSystemFlags = 0;
  if (isCapturePhaseListener) {
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }
  
  // 添加事件监听器
  addTrappedEventListener(
    target,
    domEventName,
    eventSystemFlags,
    isCapturePhaseListener,
  );
}

function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
) {
  // 创建监听器函数
  const listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
  );
  
  // 添加到DOM
  if (isCapturePhaseListener) {
    targetContainer.addEventListener(domEventName, listener, true);
  } else {
    targetContainer.addEventListener(domEventName, listener, false);
  }
}

事件注册流程图

13.2.2 dispatchEvent入口

当用户触发事件时,React的事件监听器会被调用,进入dispatchEvent流程。

createEventListenerWrapperWithPriority

根据事件类型,创建不同优先级的监听器:

javascript
// 文件:packages/react-dom-bindings/src/events/ReactDOMEventListener.js
// 行号:100-150(React 19.3.0)

export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
  // 根据事件名称获取优先级
  const eventPriority = getEventPriority(domEventName);
  
  let listenerWrapper;
  switch (eventPriority) {
    case DiscreteEventPriority:
      // 离散事件:click, keydown, input等
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case ContinuousEventPriority:
      // 连续事件:mousemove, scroll等
      listenerWrapper = dispatchContinuousEvent;
      break;
    case DefaultEventPriority:
    default:
      // 默认优先级
      listenerWrapper = dispatchEvent;
      break;
  }
  
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

dispatchEvent实现

javascript
// 文件:packages/react-dom-bindings/src/events/ReactDOMEventListener.js
// 行号:200-250(React 19.3.0)

function dispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): void {
  // 1. 获取事件目标
  const nativeEventTarget = getEventTarget(nativeEvent);
  
  // 2. 查找最近的Fiber节点
  const targetInst = getClosestInstanceFromNode(nativeEventTarget);
  
  // 3. 分发事件到React事件系统
  dispatchEventForPluginEventSystem(
    domEventName,
    eventSystemFlags,
    nativeEvent,
    targetInst,
    targetContainer,
  );
}

13.2.3 合成事件构造

React需要从原生事件构造合成事件对象,并收集事件路径上的所有处理函数。

dispatchEventForPluginEventSystem

javascript
// 文件:packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
// 行号:400-500(React 19.3.0)

export function dispatchEventForPluginEventSystem(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  let ancestorInst = targetInst;
  
  // 批量更新
  batchedUpdates(() => {
    // 分发事件
    dispatchEventsForPlugins(
      domEventName,
      eventSystemFlags,
      nativeEvent,
      ancestorInst,
      targetContainer,
    );
  });
}

function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  const nativeEventTarget = getEventTarget(nativeEvent);
  
  // 创建事件队列
  const dispatchQueue: DispatchQueue = [];
  
  // 提取事件(收集事件处理函数)
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  
  // 处理事件队列
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

extractEvents:收集事件处理函数

javascript
// 文件:packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
// 行号:550-650(React 19.3.0)

function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
): void {
  // 根据事件类型选择对应的插件
  // 例如:SimpleEventPlugin处理click、input等简单事件
  //      ChangeEventPlugin处理change事件
  //      SelectEventPlugin处理select事件
  
  // 以SimpleEventPlugin为例
  const reactName = topLevelEventsToReactNames.get(domEventName);
  if (reactName === undefined) {
    return;
  }
  
  // 创建合成事件对象
  let SyntheticEventCtor = SyntheticEvent;
  switch (domEventName) {
    case 'click':
    case 'mousedown':
    case 'mouseup':
      SyntheticEventCtor = SyntheticMouseEvent;
      break;
    case 'keydown':
    case 'keyup':
      SyntheticEventCtor = SyntheticKeyboardEvent;
      break;
    case 'focusin':
    case 'focusout':
      SyntheticEventCtor = SyntheticFocusEvent;
      break;
    // ... 其他事件类型
  }
  
  // 收集事件路径上的监听器
  const listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    eventSystemFlags & IS_CAPTURE_PHASE,
  );
  
  if (listeners.length > 0) {
    // 创建合成事件
    const event = new SyntheticEventCtor(
      reactName,
      domEventName,
      null,
      nativeEvent,
      nativeEventTarget,
    );
    
    // 添加到分发队列
    dispatchQueue.push({
      event,
      listeners,
    });
  }
}

accumulateSinglePhaseListeners:收集监听器

javascript
// 文件:packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
// 行号:700-800(React 19.3.0)

export function accumulateSinglePhaseListeners(
  targetFiber: Fiber | null,
  reactName: string | null,
  nativeEventType: string,
  isCapturePhase: boolean,
): Array<DispatchListener> {
  const captureName = reactName !== null ? reactName + 'Capture' : null;
  const reactEventName = isCapturePhase ? captureName : reactName;
  const listeners: Array<DispatchListener> = [];
  
  let instance = targetFiber;
  let lastHostComponent = null;
  
  // 从目标Fiber向上遍历到根节点
  while (instance !== null) {
    const {stateNode, tag} = instance;
    
    // 只处理HostComponent(DOM节点)
    if (tag === HostComponent && stateNode !== null) {
      lastHostComponent = stateNode;
      
      // 获取该节点上的事件处理函数
      if (reactEventName !== null) {
        const listener = getListener(instance, reactEventName);
        if (listener != null) {
          listeners.push(
            createDispatchListener(instance, listener, lastHostComponent),
          );
        }
      }
    }
    
    // 向上遍历
    instance = instance.return;
  }
  
  return listeners;
}

processDispatchQueue:执行事件处理函数

javascript
// 文件:packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
// 行号:850-900(React 19.3.0)

export function processDispatchQueue(
  dispatchQueue: DispatchQueue,
  eventSystemFlags: EventSystemFlags,
): void {
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  
  for (let i = 0; i < dispatchQueue.length; i++) {
    const {event, listeners} = dispatchQueue[i];
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
  }
}

function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
): void {
  if (inCapturePhase) {
    // 捕获阶段:从父到子(反向遍历)
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      if (event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
    }
  } else {
    // 冒泡阶段:从子到父(正向遍历)
    for (let i = 0; i < dispatchListeners.length; i++) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      if (event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
    }
  }
}

function executeDispatch(
  event: ReactSyntheticEvent,
  listener: Function,
  currentTarget: EventTarget,
): void {
  event.currentTarget = currentTarget;
  try {
    listener(event);
  } catch (error) {
    // 错误处理
  }
  event.currentTarget = null;
}

事件触发完整流程图

示例:事件冒泡与捕获

javascript
function EventPropagation() {
  const handleParentClick = (e) => {
    console.log('父元素 - 冒泡阶段');
  };
  
  const handleParentClickCapture = (e) => {
    console.log('父元素 - 捕获阶段');
  };
  
  const handleChildClick = (e) => {
    console.log('子元素 - 冒泡阶段');
    // e.stopPropagation(); // 取消注释可阻止冒泡
  };
  
  const handleChildClickCapture = (e) => {
    console.log('子元素 - 捕获阶段');
  };
  
  return (
    <div
      onClick={handleParentClick}
      onClickCapture={handleParentClickCapture}
      style={{ padding: '20px', background: 'lightblue' }}
    >
      父元素
      <button
        onClick={handleChildClick}
        onClickCapture={handleChildClickCapture}
        style={{ margin: '10px' }}
      >
        子元素
      </button>
    </div>
  );
}

// 点击按钮,输出顺序:
// 1. 父元素 - 捕获阶段
// 2. 子元素 - 捕获阶段
// 3. 子元素 - 冒泡阶段
// 4. 父元素 - 冒泡阶段

React事件与原生事件的执行顺序

javascript
function EventOrder() {
  useEffect(() => {
    // 原生事件监听器
    const button = document.getElementById('myButton');
    button.addEventListener('click', () => {
      console.log('原生事件 - 冒泡');
    });
    
    document.addEventListener('click', () => {
      console.log('document - 原生事件');
    });
  }, []);
  
  const handleReactClick = () => {
    console.log('React合成事件');
  };
  
  return (
    <button id="myButton" onClick={handleReactClick}>
      点击测试
    </button>
  );
}

// React 17+的执行顺序:
// 1. 原生事件 - 冒泡(在button上)
// 2. React合成事件(在根容器上)
// 3. document - 原生事件(在document上)

// React 16的执行顺序:
// 1. 原生事件 - 冒泡(在button上)
// 2. React合成事件(在document上)
// 3. document - 原生事件(在document上,但React已处理)

13.3 事件优先级

React的事件系统与调度系统紧密集成,不同类型的事件具有不同的优先级。

13.3.1 离散事件与连续事件

React将事件分为三类,每类对应不同的优先级。

事件优先级分类

javascript
// 文件:packages/react-dom-bindings/src/events/ReactDOMEventListener.js
// 行号:50-100(React 19.3.0)

export function getEventPriority(domEventName: DOMEventName): EventPriority {
  switch (domEventName) {
    // 离散事件(DiscreteEventPriority)
    // 用户交互,需要立即响应
    case 'click':
    case 'keydown':
    case 'keyup':
    case 'input':
    case 'change':
    case 'submit':
    case 'reset':
    case 'mousedown':
    case 'mouseup':
    case 'touchstart':
    case 'touchend':
    case 'focusin':
    case 'focusout':
      return DiscreteEventPriority;
    
    // 连续事件(ContinuousEventPriority)
    // 频繁触发,可以适当延迟
    case 'mousemove':
    case 'mouseenter':
    case 'mouseleave':
    case 'touchmove':
    case 'scroll':
    case 'wheel':
    case 'drag':
    case 'dragover':
      return ContinuousEventPriority;
    
    // 默认事件(DefaultEventPriority)
    // 其他事件
    case 'load':
    case 'error':
    case 'abort':
    case 'canplay':
    case 'canplaythrough':
    case 'durationchange':
    case 'emptied':
    case 'encrypted':
    case 'ended':
    case 'loadeddata':
    case 'loadedmetadata':
    case 'loadstart':
    case 'pause':
    case 'play':
    case 'playing':
    case 'progress':
    case 'ratechange':
    case 'seeked':
    case 'seeking':
    case 'stalled':
    case 'suspend':
    case 'timeupdate':
    case 'volumechange':
    case 'waiting':
      return DefaultEventPriority;
    
    default:
      return DefaultEventPriority;
  }
}

优先级对应关系

事件类型优先级Lane说明示例
离散事件DiscreteEventPrioritySyncLane最高优先级,同步执行click, input, keydown
连续事件ContinuousEventPriorityInputContinuousLane较高优先级,可批处理mousemove, scroll
默认事件DefaultEventPriorityDefaultLane普通优先级,可延迟load, error, media事件

13.3.2 与调度系统的集成

事件优先级会影响React的更新调度。

dispatchDiscreteEvent:离散事件处理

javascript
// 文件:packages/react-dom-bindings/src/events/ReactDOMEventListener.js
// 行号:150-200(React 19.3.0)

function dispatchDiscreteEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  container: EventTarget,
  nativeEvent: AnyNativeEvent,
) {
  // 获取之前的优先级
  const previousPriority = getCurrentUpdatePriority();
  const prevTransition = ReactCurrentBatchConfig.transition;
  ReactCurrentBatchConfig.transition = null;
  
  try {
    // 设置为离散事件优先级(最高)
    setCurrentUpdatePriority(DiscreteEventPriority);
    
    // 分发事件
    dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
  } finally {
    // 恢复之前的优先级
    setCurrentUpdatePriority(previousPriority);
    ReactCurrentBatchConfig.transition = prevTransition;
  }
}

dispatchContinuousEvent:连续事件处理

javascript
// 文件:packages/react-dom-bindings/src/events/ReactDOMEventListener.js
// 行号:250-300(React 19.3.0)

function dispatchContinuousEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  container: EventTarget,
  nativeEvent: AnyNativeEvent,
) {
  const previousPriority = getCurrentUpdatePriority();
  const prevTransition = ReactCurrentBatchConfig.transition;
  ReactCurrentBatchConfig.transition = null;
  
  try {
    // 设置为连续事件优先级
    setCurrentUpdatePriority(ContinuousEventPriority);
    
    // 分发事件
    dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
  } finally {
    setCurrentUpdatePriority(previousPriority);
    ReactCurrentBatchConfig.transition = prevTransition;
  }
}

优先级如何影响更新

当事件处理函数中调用setState时,更新会继承当前的事件优先级:

javascript
function Counter() {
  const [count, setCount] = useState(0);
  
  // 离散事件:立即更新
  const handleClick = () => {
    setCount(c => c + 1); // DiscreteEventPriority -> SyncLane
    // 更新会同步执行
  };
  
  // 连续事件:可能被批处理
  const handleMouseMove = () => {
    setCount(c => c + 1); // ContinuousEventPriority -> InputContinuousLane
    // 多次调用可能被合并
  };
  
  return (
    <div>
      <button onClick={handleClick}>点击: {count}</button>
      <div onMouseMove={handleMouseMove}>移动鼠标: {count}</div>
    </div>
  );
}

事件优先级与Lane的转换

javascript
// 文件:packages/react-reconciler/src/ReactEventPriorities.js
// 行号:50-100(React 19.3.0)

export function eventPriorityToLane(eventPriority: EventPriority): Lane {
  switch (eventPriority) {
    case DiscreteEventPriority:
      return SyncLane;
    case ContinuousEventPriority:
      return InputContinuousLane;
    case DefaultEventPriority:
      return DefaultLane;
    case IdleEventPriority:
      return IdleLane;
    default:
      return NoLane;
  }
}

export function lanesToEventPriority(lanes: Lanes): EventPriority {
  const lane = getHighestPriorityLane(lanes);
  
  if (!isHigherEventPriority(DiscreteEventPriority, lane)) {
    return DiscreteEventPriority;
  }
  if (!isHigherEventPriority(ContinuousEventPriority, lane)) {
    return ContinuousEventPriority;
  }
  if (includesNonIdleWork(lane)) {
    return DefaultEventPriority;
  }
  return IdleEventPriority;
}

优先级调度流程图

示例:不同优先级的更新行为

javascript
function PriorityDemo() {
  const [count, setCount] = useState(0);
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  // 离散事件:高优先级,立即更新
  const handleClick = () => {
    console.log('点击事件开始');
    setCount(c => c + 1);
    console.log('点击事件结束');
    // 输出:
    // 点击事件开始
    // 点击事件结束
    // (然后立即重新渲染)
  };
  
  // 连续事件:较低优先级,可能被节流
  const handleMouseMove = (e) => {
    setPosition({ x: e.clientX, y: e.clientY });
    // 频繁触发,但React会智能批处理
  };
  
  // 使用useTransition降低优先级
  const [isPending, startTransition] = useTransition();
  const handleLowPriorityUpdate = () => {
    startTransition(() => {
      setCount(c => c + 1);
      // 这个更新会被标记为低优先级
      // 可以被高优先级更新打断
    });
  };
  
  return (
    <div onMouseMove={handleMouseMove}>
      <h1>Count: {count}</h1>
      <p>Position: ({position.x}, {position.y})</p>
      
      <button onClick={handleClick}>
        高优先级更新
      </button>
      
      <button onClick={handleLowPriorityUpdate}>
        低优先级更新 {isPending && '(pending)'}
      </button>
    </div>
  );
}

批处理与优先级

React 18引入了自动批处理(Automatic Batching),但优先级仍然影响批处理的行为:

javascript
function BatchingDemo() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  
  const handleClick = () => {
    // React 18:自动批处理
    setCount(c => c + 1);
    setFlag(f => !f);
    // 只会触发一次重新渲染
  };
  
  const handleClickWithTimeout = () => {
    setTimeout(() => {
      // React 18:仍然会批处理
      setCount(c => c + 1);
      setFlag(f => !f);
      // 只会触发一次重新渲染
    }, 1000);
  };
  
  const handleClickWithFetch = async () => {
    const data = await fetch('/api/data');
    // React 18:异步操作后仍然批处理
    setCount(c => c + 1);
    setFlag(f => !f);
    // 只会触发一次重新渲染
  };
  
  console.log('渲染次数:', ++renderCount);
  
  return (
    <div>
      <p>Count: {count}, Flag: {flag.toString()}</p>
      <button onClick={handleClick}>同步批处理</button>
      <button onClick={handleClickWithTimeout}>异步批处理</button>
      <button onClick={handleClickWithFetch}>Fetch批处理</button>
    </div>
  );
}

事件优先级的实际应用

理解事件优先级对于优化React应用性能至关重要:

javascript
function SearchWithDebounce() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();
  
  const handleInputChange = (e) => {
    const value = e.target.value;
    
    // 高优先级:立即更新输入框
    setQuery(value);
    
    // 低优先级:延迟更新搜索结果
    startTransition(() => {
      // 模拟搜索
      const filtered = performExpensiveSearch(value);
      setResults(filtered);
    });
  };
  
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleInputChange}
        placeholder="搜索..."
      />
      {isPending && <div>搜索中...</div>}
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

function performExpensiveSearch(query) {
  // 模拟耗时的搜索操作
  const items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`,
  }));
  return items.filter(item => 
    item.name.toLowerCase().includes(query.toLowerCase())
  );
}

在这个例子中:

  1. 输入框更新使用离散事件优先级(DiscreteEventPriority),确保用户输入不会卡顿
  2. 搜索结果更新使用Transition优先级(较低),可以被输入事件打断
  3. 这样即使搜索很慢,用户仍然可以流畅地输入

本章小结

本章深入分析了React的事件系统,主要内容包括:

  1. 合成事件的设计理念

    • 解决浏览器兼容性问题
    • 通过事件委托优化性能
    • 与React更新机制深度集成
  2. 事件委托机制

    • React 17+将事件委托到根容器
    • 减少内存占用,提高性能
    • 支持多个React应用共存
  3. 事件注册与触发流程

    • listenToAllSupportedEvents在根容器注册所有事件
    • dispatchEvent处理原生事件
    • extractEvents构造合成事件并收集监听器
    • processDispatchQueue按顺序执行事件处理函数
  4. 事件优先级

    • 离散事件(DiscreteEventPriority):最高优先级,同步执行
    • 连续事件(ContinuousEventPriority):较高优先级,可批处理
    • 默认事件(DefaultEventPriority):普通优先级,可延迟
    • 事件优先级与Lane模型的转换

React事件系统是React与用户交互的桥梁,理解其工作原理有助于:

  • 正确处理事件冒泡和捕获
  • 优化事件处理性能
  • 合理使用事件优先级
  • 避免常见的事件处理陷阱

思考题

  1. 为什么React 17将事件委托从document改为根容器?这对多个React应用共存有什么影响?

  2. React的合成事件与原生事件有什么区别?在什么情况下需要使用e.nativeEvent

  3. 事件优先级如何影响React的更新调度?离散事件和连续事件的处理有什么不同?

  4. 如何在React中正确处理事件冒泡?e.stopPropagation()e.preventDefault()有什么区别?

  5. React 17移除事件池机制的原因是什么?这对开发者有什么影响?


下一章预告

在进阶篇中,我们将学习React Compiler的设计与实现,了解React如何通过编译时优化提升运行时性能。