React 合成事件(SyntheticEvent)

React 的事件处理为什么看起来像原生事件,又不完全一样?

先记住一句话:合成事件是 React 对浏览器事件的统一封装和分发机制。它提供类似原生事件的 API,并由 React 负责把事件交给对应组件处理;它不是一个新的浏览器事件,也不代表所有事件都只绑定在一个节点上。

1. 什么是合成事件?

JSX 中这样写的 onClickonChange 等回调,接收到的通常是 React 的 SyntheticEvent

function Button() {
  function handleClick(event) {
    console.log(event.type);   // "click"
    console.log(event.target); // 原生事件目标
    event.preventDefault();
  }

  return <button onClick={handleClick}>提交</button>;
}

合成事件会尽量提供一致的接口,例如:

  • typetargetcurrentTarget
  • preventDefault()
  • stopPropagation()
  • bubblesdefaultPrevented
  • nativeEvent,用于访问底层原生事件。

React 还会对部分事件做符合 React 习惯的处理。例如 React 的 onChange 在文本输入场景中会更及时地反映值变化,不应简单当作浏览器原生 change 事件的逐字翻译。

2. React 为什么使用自己的事件系统?

统一开发接口

不同浏览器历史上存在事件属性和行为差异。React 通过统一的事件对象和插件/分发机制,让组件代码不需要直接处理大部分兼容性细节。

统一 React 树中的传播

React 事件会按照 React 组件树的捕获和冒泡规则分发:

<div onClick={() => console.log("parent")}>
  <button onClick={() => console.log("child")}>点击</button>
</div>

点击按钮时,通常会先执行子节点处理器,再冒泡到父节点处理器。捕获阶段则使用 onClickCapture

<div onClickCapture={() => console.log("capture")}>
  <button onClick={() => console.log("bubble")}>点击</button>
</div>

事件委托与调度

React 通常会在较高层级监听事件,再根据事件目标和 React Fiber 树找到对应的组件回调。这样可以统一事件分发,也减少在大量节点上重复注册监听器的需要。

但不要把它概括成“所有事件都只绑定在 root 上”:部分事件有特殊处理,具体委托方式也会随 React 版本和事件类型变化。事件委托更重要的价值是统一协调,并不应被当成组件性能优化的唯一依据。

3. React 17 的事件委托变化

React 16 及以前,许多事件委托在 document;React 17 起,React DOM 通常把事件入口放在对应的 React Root 容器上:

React 16:document
React 17+:React Root 容器

这样做的主要好处是:

  • 多个 React Root 可以减少彼此影响;
  • React 嵌入已有 jQuery 或其他框架页面时更容易隔离;
  • 同一页面可以逐步迁移不同版本或不同应用。

这不意味着原生事件不再经过 document,也不意味着 React 事件和原生事件一定拥有相同的传播顺序。混用时应通过实际绑定位置和事件类型判断行为。

4.preventDefaultstopPropagation

阻止默认行为

preventDefault() 阻止浏览器的默认动作,例如链接跳转或表单提交:

function Form() {
  function handleSubmit(event) {
    event.preventDefault();
    saveForm();
  }

  return <form onSubmit={handleSubmit}>...</form>;
}

阻止传播

stopPropagation() 阻止事件继续沿 React 事件路径传播:

function ListItem({ onOpen }) {
  return (
    <div onClick={onOpen}>
      <button
        onClick={(event) => {
          event.stopPropagation();
          removeItem();
        }}
      >
        删除
      </button>
    </div>
  );
}

它不等于“取消默认行为”。如果按钮位于链接中,通常需要根据需求同时调用 preventDefault()

5. 合成事件与原生事件混用

可以通过 nativeEvent 访问底层原生事件:

function handleClick(event) {
  console.log(event.nativeEvent);
}

也可以使用 ref 注册原生监听器,但需要注意三个问题:

  1. React 事件和原生事件可能在不同节点上监听,执行顺序不能只凭经验判断;
  2. 原生事件的 stopPropagation() 与合成事件的传播路径可能互相影响,复杂场景容易出现顺序问题;
  3. 原生监听器必须在 cleanup 中移除,否则会重复绑定或造成内存泄漏。
function Panel() {
  const panelRef = useRef(null);

  useEffect(() => {
    const element = panelRef.current;
    if (!element) return;

    function handleScroll() {
      // 原生事件逻辑
    }

    element.addEventListener("scroll", handleScroll);
    return () => element.removeEventListener("scroll", handleScroll);
  }, []);

  return <div ref={panelRef} />;
}

如果不需要原生 API 的特殊选项(例如 passivecapture),优先使用 JSX 的 React 事件处理器。

6. Event Pooling 事件池

React 16 及更早版本曾复用 SyntheticEvent 对象。事件回调返回后,对象属性可能被清空,异步代码不能直接依赖它:

// React 16 及更早版本的风险写法
function handleChange(event) {
  setTimeout(() => {
    console.log(event.target.value);
  }, 0);
}

React 17 起,Web 环境移除了旧的事件池机制,通常可以在异步回调中继续读取事件对象。不过更稳妥的做法仍然是及时提取需要的数据:

function handleChange(event) {
  const value = event.target.value;
  setTimeout(() => {
    console.log(value);
  }, 0);
}

这也避免了异步代码持有过大的事件对象或依赖事件目标已经存在。

7. 常见事件写法

React 事件名使用驼峰

<button onClick={handleClick} />
<input onChange={handleChange} />
<div onMouseEnter={handleEnter} />

不是 HTML 中的全小写 onclick。事件回调也应传入函数,而不是立即调用:

// ✅ 点击时调用
<button onClick={handleClick} />

// ❌ 渲染时就调用
<button onClick={handleClick()} />

传递参数

<button onClick={(event) => handleDelete(id, event)}>
  删除
</button>

如果不需要事件对象,也可以只传业务参数:

<button onClick={() => handleDelete(id)}>删除</button>

8. 常见误区

合成事件就是原生事件的完全复制

不是。它提供兼容的常用接口,但事件类型、传播、currentTarget 和委托方式仍由 React 管理。需要原生事件特性时,明确使用 nativeEvent 或原生监听器。

stopPropagation 会阻止所有监听器

它主要阻止事件继续传播,不等于阻止同一节点上的其他监听器;若要阻止同节点后续原生监听器,需要使用原生事件的 stopImmediatePropagation,但在 React 中不建议随意混用。

React 会把所有事件都绑定到一个 root

事件委托存在例外,且多个 Root 各自有事件入口。不要依赖“全局只有一个 React 监听器”的实现细节。

React 事件一定比原生事件先执行

不一定。顺序取决于原生监听器的节点、捕获/冒泡阶段、React Root 位置以及事件类型。混用时应减少跨系统的传播控制,必要时通过小型示例验证。

总结

概念作用
SyntheticEventReact 对浏览器事件的统一封装
React 事件委托统一分发 React 树中的事件处理器
preventDefault()阻止浏览器默认行为
stopPropagation()阻止事件继续传播
nativeEvent访问底层原生事件
React 17+React DOM 事件入口通常位于对应 Root 容器
Event PoolingReact 17 Web 环境已移除旧事件池机制

面试时可以这样回答:React 通过 SyntheticEvent 统一事件接口,并结合事件委托把原生事件分发给 React 树中的处理器。React 17 起事件入口通常从 document 移到 Root 容器,改善了多 Root 和微前端场景。preventDefault 用于阻止默认行为,stopPropagation 用于阻止传播;如果混用原生事件,要特别注意监听位置和执行顺序。