这是 React 面试中的高频题,尤其是中高级前端岗位。可以从 React 合成事件机制 → 与原生事件区别 → 阻止冒泡和默认行为 三部分回答。
一、什么是 React 合成事件(SyntheticEvent)
React 为了兼容不同浏览器的事件行为,实现了一套自己的事件系统,称为 合成事件(SyntheticEvent) 。
例如:
javascript
function App() {
const handleClick = (e) => {
console.log(e);
};
return <button onClick={handleClick}>点击</button>;
}
这里的 e 不是浏览器原生 Event,而是 React 封装后的 SyntheticEvent。
本质:
css
SyntheticEvent {
nativeEvent: MouseEvent,
target,
currentTarget,
stopPropagation,
preventDefault
}
React 内部会:
- 监听原生事件
- 封装成 SyntheticEvent
- 统一派发给对应组件
好处:
- 抹平浏览器差异
- 提高兼容性
- 方便 React 统一管理事件
二、React 合成事件工作机制
React16及以前
React 采用 事件委托(Event Delegation)
例如:
xml
<div>
<button onClick={fn1}>按钮1</button>
<button onClick={fn2}>按钮2</button>
</div>
实际上不会给每个 button 绑定 click。
React 会:
javascript
document.addEventListener('click', ...)
统一绑定到 document。
触发时:
css
button点击
↓
document监听到
↓
React找到对应Fiber节点
↓
执行onClick
优势:
- 减少事件监听器数量
- 节省内存
- 提高性能
React17+
React 修改了事件委托位置。
以前:
javascript
document
现在:
root容器
例如:
javascript
ReactDOM.createRoot(
document.getElementById('root')
);
事件绑定到:
bash
<div id="root"></div>
而不是:
javascript
document
原因:
解决多个 React 应用共存时的事件冲突问题。
三、React 合成事件执行流程
假设:
xml
<div onClick={parent}>
<button onClick={child}>
点击
</button>
</div>
点击按钮:
markdown
原生事件触发
↓
React收集事件
↓
捕获阶段执行
↓
目标阶段执行
↓
冒泡阶段执行
执行顺序:
scss
child()
parent()
与原生 DOM 一致。
四、React合成事件和原生事件区别
1. 事件对象不同
React:
ini
onClick={(e)=>{
console.log(e);
}}
得到:
SyntheticEvent
原生:
javascript
button.addEventListener('click',(e)=>{
console.log(e);
})
得到:
MouseEvent
2. 获取原生事件方式不同
React:
ini
const handleClick = (e) => {
console.log(e.nativeEvent);
};
输出:
MouseEvent
3. 绑定方式不同
React:
ini
<button onClick={handleClick}>
原生:
less
button.addEventListener('click', handleClick)
4. 事件委托位置不同
React16:
javascript
document
React17+:
root节点
原生:
scss
element.addEventListener(...)
直接绑定到当前元素。
5. 性能优化不同
原生:
yaml
1000个按钮
1000个监听器
React:
统一监听
事件委托
监听器数量更少。
五、事件冒泡
DOM事件传播流程:
捕获阶段
↓
目标阶段
↓
冒泡阶段
例如:
ini
<div onClick={() => console.log('父')}>
<button onClick={() => console.log('子')}>
点击
</button>
</div>
输出:
子
父
因为发生了冒泡。
六、阻止事件冒泡
React中
ini
const handleChild = (e) => {
e.stopPropagation();
console.log('子');
};
javascript
<div onClick={() => console.log('父')}>
<button onClick={handleChild}>
点击
</button>
</div>
结果:
子
父元素不会执行。
原生JS
javascript
button.addEventListener('click', (e) => {
e.stopPropagation();
});
同样效果。
七、阻止默认行为
例如:
ini
<a href="https://google.com">
跳转
</a>
阻止跳转:
ini
const handleClick = (e) => {
e.preventDefault();
};
ini
<a href="https://google.com" onClick={handleClick}>
跳转
</a>
八、React捕获阶段事件
React提供:
onClickCapture
例如:
ini
<div onClickCapture={() => console.log('父捕获')}>
<button onClick={() => console.log('子冒泡')}>
点击
</button>
</div>
输出:
父捕获
子冒泡
执行顺序:
markdown
Capture
↓
Target
↓
Bubble
九、React18面试加分点
事件池(Event Pooling)
React16以前:
ini
const handleClick = (e) => {
setTimeout(() => {
console.log(e.target);
});
};
可能得到:
csharp
null
因为 React 使用了事件池复用对象。
解决:
ini
e.persist();
React17以后:
事件池机制已经移除。
javascript
setTimeout(() => {
console.log(e.target);
});
正常输出。
面试标准回答(2分钟版)
React 的事件机制采用 SyntheticEvent 合成事件,它是 React 对浏览器原生事件的封装,用来抹平浏览器差异并统一管理事件。
React 并不会给每个 DOM 节点都绑定事件,而是采用 事件委托 。React16 及以前统一绑定到 document,React17 以后改为绑定到 React Root 容器,从而解决多个 React 应用之间的事件冲突问题。
合成事件对象提供了与原生事件一致的 API,比如 preventDefault、stopPropagation,同时可以通过 event.nativeEvent 获取原生事件对象。
事件传播机制与 DOM 一致,分为 捕获阶段、目标阶段、冒泡阶段。阻止冒泡可以使用:
ini
e.stopPropagation();
阻止默认行为可以使用:
ini
e.preventDefault();
React17 以后还移除了事件池机制,不再需要使用 e.persist()。这样回答基本可以覆盖大部分 React 面试场景。