React 合成事件机制

React 事件委托和事件池的工作原理

问题

解释 React 合成事件机制,包括事件委托和事件池。

解答

什么是合成事件

React 实现了一套自己的事件系统,将浏览器原生事件封装成 SyntheticEvent 对象,提供跨浏览器一致的 API。

function Button() {
  const handleClick = (e) => {
    // e 是 SyntheticEvent,不是原生事件
    console.log(e.constructor.name); // SyntheticBaseEvent
    console.log(e.nativeEvent);      // 原生事件对象
  };

  return <button onClick={handleClick}>点击</button>;
}

事件委托

React 不会把事件绑定到具体 DOM 节点,而是统一绑定到根容器,通过事件冒泡统一处理。

// React 17+ 绑定到 root container
// React 16 及之前绑定到 document

function App() {
  return (
    <div onClick={() => console.log('div')}>
      <button onClick={() => console.log('button')}>
        点击
      </button>
    </div>
  );
}

// 点击 button 输出:
// button
// div

事件委托的执行流程:

function Demo() {
  const rootRef = React.useRef(null);

  React.useEffect(() => {
    // 原生事件 - 捕获阶段
    rootRef.current.addEventListener('click', () => {
      console.log('原生捕获');
    }, true);

    // 原生事件 - 冒泡阶段
    rootRef.current.addEventListener('click', () => {
      console.log('原生冒泡');
    });
  }, []);

  return (
    <div ref={rootRef}>
      <button
        onClickCapture={() => console.log('React 捕获')}
        onClick={() => console.log('React 冒泡')}
      >
        点击
      </button>
    </div>
  );
}

// 点击输出顺序:
// 原生捕获
// React 捕获
// React 冒泡
// 原生冒泡

事件池(React 16 及之前)

React 16 使用事件池复用事件对象,事件回调执行后属性会被清空:

// React 16 的行为
function OldBehavior() {
  const handleClick = (e) => {
    console.log(e.type); // 'click'
    
    setTimeout(() => {
      console.log(e.type); // null(已被回收)
    }, 0);

    // 需要调用 persist() 保留事件
    // e.persist();
  };

  return <button onClick={handleClick}>点击</button>;
}

React 17+ 移除了事件池:

// React 17+ 的行为
function NewBehavior() {
  const handleClick = (e) => {
    console.log(e.type); // 'click'
    
    setTimeout(() => {
      console.log(e.type); // 'click'(正常访问)
    }, 0);
  };

  return <button onClick={handleClick}>点击</button>;
}

阻止事件传播

function StopPropagation() {
  const handleParent = () => console.log('parent');
  
  const handleChild = (e) => {
    e.stopPropagation(); // 阻止合成事件冒泡
    console.log('child');
  };

  return (
    <div onClick={handleParent}>
      <button onClick={handleChild}>点击</button>
    </div>
  );
}

// 只输出:child

合成事件与原生事件混用

function MixedEvents() {
  const buttonRef = React.useRef(null);

  React.useEffect(() => {
    // 原生事件绑定在目标元素
    buttonRef.current.addEventListener('click', (e) => {
      e.stopPropagation(); // 阻止原生事件冒泡
      console.log('原生事件');
    });

    document.addEventListener('click', () => {
      console.log('document');
    });
  }, []);

  return (
    <button
      ref={buttonRef}
      onClick={() => console.log('React 事件')}
    >
      点击
    </button>
  );
}

// React 17+:原生事件 -> React 事件
// React 16:原生事件(document 上的 React 事件被阻止)

关键点

  • 事件委托:React 17+ 将事件绑定到 root container,React 16 绑定到 document
  • 合成事件:跨浏览器封装,通过 e.nativeEvent 访问原生事件
  • 事件池已移除:React 17 移除事件池,不再需要 e.persist()
  • 执行顺序:原生捕获 → React 捕获 → React 冒泡 → 原生冒泡
  • 混用注意:原生事件的 stopPropagation 会影响 React 事件(React 16 影响更大)