React 合成事件原理:从事件委托到 React 17 的重大改进

208 阅读4分钟

前言

在前端开发中,事件处理是一个非常重要的概念。React 作为最流行的前端框架之一,为了实现跨浏览器兼容性和更好的性能,设计了一套自己的事件系统——合成事件(Synthetic Event)

本文将深入解析 React 合成事件的工作原理,以及 React 17 中关于合成事件的重大改进,帮助你更好地理解 React 的事件机制。

什么是 React 合成事件?

React 合成事件是 React 封装的一套事件系统,它是对原生 DOM 事件的封装。当你在 JSX 中写 onClickonChange 等事件时,实际上绑定的是 React 的合成事件,而不是原生的 DOM 事件。

function MyComponent() {
  const handleClick = (e) => {
    console.log('这是一个 React 合成事件');
    console.log('事件对象:', e);
  };

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

React 合成事件的核心原理

1. 事件委托机制

React 合成事件的核心是事件委托。React 不会为每个 DOM 节点都绑定事件监听器,而是将所有事件委托到一个统一的节点上。

  • React 17 之前:事件委托到 document 节点;
  • React 17 之后:事件委托到应用的根节点(root);

2. 事件处理流程

话不多说,先上图:

image.png

让我们通过一个具体的例子来理解 React 合成事件的处理流程:

class TaskEvent extends React.PureComponent {
  render() {
    return (
      <div
        onClick={() => {
          console.log('我是注册事件');
        }}
      >
        点击我
      </div>
    );
  }
}

当用户点击这个 div 时,整个事件处理流程如下:

阶段一:事件注册

  1. 在组件创建和更新时,React 会将事件(listenTo)委托注册到根节点;
  2. 事件信息存储在 listenerBank(事件池)中;

阶段二:事件触发

  1. 用户点击时,原生 DOM 事件首先触发;
  2. 原生事件通过事件冒泡机制,冒泡到根节点;
  3. 根节点接收到冒泡事件后,找到目标 DOM 元素;
  4. 通过 DOM 元素上的 __reactInternalInstance 属性,找到对应的 React 元素;

阶段三:事件处理

  1. React 从当前 React 元素开始,向上遍历所有父组件;
  2. 收集所有相关的事件回调函数,存储在 eventQueue 中;
  3. 根据事件类型构建合成事件对象;
  4. 按顺序执行 eventQueue 中的回调函数;

React 17 的重大改进

问题背景

在 React 17 之前,合成事件存在一个严重的问题:e.stopPropagation() 无法阻止原生事件的冒泡且不同版本的 React 应用共存会相互影响

// React 17 之前的问题示例
function App() {
  useEffect(() => {
    // 在 document 上添加原生事件监听器
    const handleDocumentClick = () => {
      console.log('document 被点击了!');
    };
    
    document.addEventListener('click', handleDocumentClick);
    
    return () => {
      document.removeEventListener('click', handleDocumentClick);
    };
  }, []);

  return (
    <div onClick={() => console.log('外层 div 被点击')}>
      <button
        onClick={(e) => {
          e.stopPropagation(); // 这个只能阻止合成事件冒泡
          console.log('按钮被点击');
        }}
      >
        点击我
      </button>
    </div>
  );
}

在上面的例子中,当用户点击按钮时,输出顺序会是:

  1. "按钮被点击";
  2. "外层 div 被点击" (被阻止,不会输出);
  3. "document 被点击了!" (仍然会输出,因为原生事件冒泡到了 document);

React 17 的解决方案

React 17 通过改变事件委托的宿主来解决这个问题。

// React 17 之前
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
// 事件委托到 document

// React 17 之后
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
// 事件委托到 root 节点

这个修复的关键在于事件委托节点的层级

  1. React 17 之前的问题

    • 所有 React 应用的事件都委托到 document
    • 当调用 e.stopPropagation() 时,只能阻止合成事件在 React 内部的冒泡;
    • 但原生事件仍然会冒泡到 document,因为 document 是原生事件的监听者;
  2. React 17 之后的解决方案

    • 事件委托到应用的根节点(通常是 #root 或 #app);
    • 当调用 e.stopPropagation() 时,合成事件不会冒泡到根节点;
    • 由于根节点是 React 事件系统的边界,原生事件不会继续向上冒泡到 document
    • 这样就实现了真正的事件冒泡阻止。

总结

React 合成事件是 React 框架的重要组成部分,它通过事件委托机制提供了跨浏览器兼容性和更好的性能。React 17 的改进解决了事件冒泡控制的问题,使得事件处理更加可靠和可预测。

理解 React 合成事件的原理,不仅有助于我们更好地使用 React,还能帮助我们避免一些常见的事件处理陷阱。在实际开发中,我们应该充分利用 React 17 的改进,编写更加健壮的事件处理代码。

参考资料