← 返回题目列表

React 合成事件是什么?和原生事件有什么区别?

中等 第 18 / 27 题 更新于 2026/07/29
React合成事件事件委托DOM事件

简化版

React 合成事件是对浏览器原生事件的跨浏览器封装,并通过事件委托统一管理。它让事件 API 更一致,也让 React 能在事件中做批处理、优先级调度等工作。React 17 后事件委托从 document 调整到根容器。

详细版

在 React 中写的 onClick 不是简单等同于给 DOM 直接绑定 onclick。 React 会把事件包装成合成事件对象,并在根容器上统一监听,再按组件树分发。

function Button() {
  return <button onClick={(e) => console.log(e.currentTarget)}>保存</button>
}

合成事件提供类似原生事件的接口,如 preventDefaultstopPropagation。React 事件名使用驼峰,例如 onClick。面试中要注意版本差异:旧版本委托到 document,React 17 起委托到 root 容器,便于多版本 React 共存和渐进升级。

完整版教学

一、合成事件先解决浏览器差异

不同浏览器历史上事件对象、冒泡行为、属性名称有差异。 React 希望开发者写一套统一事件 API。 于是它封装了 SyntheticEvent,让你大多数时候不用关心浏览器细节。

<button onClick={(event) => {
  event.preventDefault()
  console.log(event.type)
}}>
  提交
</button>
维度合成事件原生事件
绑定写法onClickaddEventListener('click')
封装层React 包装浏览器直接提供
调度能力可接入 React 更新不经过 React
兼容处理React 统一自己处理

对业务开发来说,它最直接的价值是写法稳定。 对 React 内部来说,它还是更新调度的重要入口。

二、事件委托减少大量监听器

React 不会给每个按钮都直接绑定一个原生 click 监听器。 它通常在根容器统一监听事件,再根据事件目标和组件树关系分发给对应处理函数。 这就是事件委托。

root 监听 click
└─ div
   └─ button 被点击
      -> React 找到对应 onClick
      -> 按捕获/冒泡顺序调用处理函数

如果页面有 1000 个按钮,逐个绑定监听器会增加管理成本。 委托模型让监听和解绑更集中,也便于 React 插入批处理和优先级逻辑。

三、React 17 改了委托挂载点

React 16 及以前很多事件委托在 document 上。 React 17 起事件委托挂到 root 容器。 这个变化对普通业务不明显,但对微前端、多 React 版本共存、渐进升级很重要。

createRoot(document.getElementById('root')!).render(<App />)
旧思路:
document 统一监听

React 17+:
#root 容器统一监听

如果一个页面里有两个 React 根,事件系统挂在各自根容器上更容易隔离。 这也是 React 17 被称为“无新特性升级版本”时仍然重要的原因之一。

四、合成事件和原生事件传播不完全等价

React 的事件回调按 React 事件系统分发。 原生事件按真实 DOM 路径传播。 大多数时候两者结果一致,但 Portal、多个 root、手动 addEventListener 等场景会让差异变明显。

useEffect(() => {
  document.addEventListener('click', nativeHandler)
  return () => document.removeEventListener('click', nativeHandler)
}, [])

如果你同时使用 React onClick 和原生 document.addEventListener,要特别注意调用顺序和停止传播的边界。 stopPropagation 在 React 合成事件中阻止 React 分发,不一定等价于你想象中对所有原生监听器都完全隔离。

五、事件中的状态更新会参与批处理

React 在事件处理函数中可以把多个状态更新合并成一次渲染。 React 18 的自动批处理范围更广,不只局限于 React 事件。 事件系统是理解批处理的一个入口。

function handleClick() {
  setCount(c => c + 1)
  setFlag(f => !f)
}

上面两个更新通常会合并为一次渲染。 如果一个页面点击按钮后触发 3 个 state 更新,批处理能避免 3 次独立渲染。 这也是 React 自己接管事件系统的原因之一。

六、旧版本事件池化已经不再是重点

React 早期为了性能复用事件对象,异步访问事件属性可能拿到空值,需要 event.persist()。 React 17 起 Web 端不再使用事件池化。 面试中如果提到事件池,要说明这是旧版本历史点。

function handleClick(e) {
  setTimeout(() => {
    console.log(e.type)
  }, 100)
}

记忆钩子:合成事件是 React 给浏览器事件套的统一前台,背后还能接批处理和调度。

这题最容易讲散。 按“封装、委托、版本变化、批处理”四个点组织就清楚。

七、常见误区与追问

  • 误区:React onClick 就是 DOM onclick。 它经过 React 合成事件系统封装和分发,不是简单直接绑定。
  • 误区:React 事件永远委托到 document。 React 17 起主要委托到根容器,便于多 root 和渐进升级。
  • 误区:合成事件和原生事件没有任何差别。 Portal、多 root、原生监听混用时,传播和顺序需要仔细判断。
  • 追问:为什么要做事件委托? 减少监听器管理成本,并让 React 能统一处理分发、批处理和优先级。
  • 追问:event.persist 现在还常用吗? React 17 后 Web 端不再事件池化,通常不需要。
  • 追问:如何监听原生事件? 在 effect 中使用 addEventListener,并在清理函数中移除。

八、加强记忆

合成事件别只记“跨浏览器”。它还有事件委托、统一分发、批处理入口和版本变化。React 17 前后委托挂载点不同;业务里混用原生事件时,要注意传播路径和清理。