React 合成事件是什么?和原生事件有什么区别?
简化版
React 合成事件是对浏览器原生事件的跨浏览器封装,并通过事件委托统一管理。它让事件 API 更一致,也让 React 能在事件中做批处理、优先级调度等工作。React 17 后事件委托从 document 调整到根容器。
详细版
在 React 中写的 onClick 不是简单等同于给 DOM 直接绑定 onclick。
React 会把事件包装成合成事件对象,并在根容器上统一监听,再按组件树分发。
function Button() {
return <button onClick={(e) => console.log(e.currentTarget)}>保存</button>
}
合成事件提供类似原生事件的接口,如 preventDefault、stopPropagation。React 事件名使用驼峰,例如 onClick。面试中要注意版本差异:旧版本委托到 document,React 17 起委托到 root 容器,便于多版本 React 共存和渐进升级。
完整版教学
一、合成事件先解决浏览器差异
不同浏览器历史上事件对象、冒泡行为、属性名称有差异。 React 希望开发者写一套统一事件 API。 于是它封装了 SyntheticEvent,让你大多数时候不用关心浏览器细节。
<button onClick={(event) => {
event.preventDefault()
console.log(event.type)
}}>
提交
</button>
| 维度 | 合成事件 | 原生事件 |
|---|---|---|
| 绑定写法 | onClick | addEventListener('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 前后委托挂载点不同;业务里混用原生事件时,要注意传播路径和清理。