合成事件系统
React 事件机制的设计与实现原理
1. 什么是合成事件?
1.1 核心概念
合成事件 (SyntheticEvent) 是 React 对原生 DOM 事件的跨浏览器封装。
jsx
function Button() {
const handleClick = (event) => {
// event 是 SyntheticEvent,不是原生 Event
console.log(event); // SyntheticBaseEvent
console.log(event.nativeEvent); // 原生 MouseEvent
console.log(event.target); // 触发元素
console.log(event.currentTarget); // 绑定元素
};
return <button onClick={handleClick}>Click</button>;
}1.2 为什么需要合成事件?
┌─────────────────────────────────────────────────────────────────┐
│ 合成事件的优势 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 跨浏览器兼容 │
│ 统一不同浏览器的事件差异 │
│ │
│ 2. 性能优化 │
│ 事件委托,减少事件监听器数量 │
│ │
│ 3. 统一事件流 │
│ 所有事件行为一致 │
│ │
│ 4. 更好的控制 │
│ 可以实现优先级调度等特性 │
│ │
└─────────────────────────────────────────────────────────────────┘2. 事件委托机制
2.1 React 16 的事件委托
┌─────────────────────────────────────────────────────────────────┐
│ React 16 事件委托 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ document │
│ │ │
│ └── 所有事件监听器都绑定在 document 上 │
│ │
│ <div id="root"> │
│ <App> │
│ <button onClick={...}> ← 事件冒泡到 document │
│ │
└─────────────────────────────────────────────────────────────────┘2.2 React 17+ 的变化
┌─────────────────────────────────────────────────────────────────┐
│ React 17+ 事件委托 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ document │
│ │ │
│ <div id="root"> ← 事件监听器绑定在 root 容器上 │
│ <App> │
│ <button onClick={...}> │
│ │
│ 优势: │
│ - 多个 React 版本可以共存 │
│ - 与其他库的事件系统兼容更好 │
│ - 微前端场景更友好 │
│ │
└─────────────────────────────────────────────────────────────────┘2.3 事件触发流程
javascript
// 用户点击 button
// 1. 原生事件冒泡到 root 容器
// 2. React 收集从 target 到 root 路径上所有的 onClick 处理函数
// 3. 按顺序执行(模拟冒泡或捕获)
// 简化的内部实现
function dispatchEvent(nativeEvent) {
const target = nativeEvent.target;
const path = [];
// 收集路径上所有 fiber
let fiber = getFiberFromNode(target);
while (fiber) {
path.push(fiber);
fiber = fiber.return;
}
// 捕获阶段(从上到下)
for (let i = path.length - 1; i >= 0; i--) {
const onClickCapture = path[i].props.onClickCapture;
if (onClickCapture) onClickCapture(syntheticEvent);
}
// 冒泡阶段(从下到上)
for (let i = 0; i < path.length; i++) {
const onClick = path[i].props.onClick;
if (onClick) onClick(syntheticEvent);
}
}3. 事件池 (已废弃)
3.1 React 16 的事件池
jsx
// React 16 中,SyntheticEvent 会被复用
function handleClick(event) {
console.log(event.type); // 'click'
setTimeout(() => {
console.log(event.type); // null (事件已被回收!)
}, 0);
}
// 需要 persist() 保持引用
function handleClick(event) {
event.persist(); // 从池中移除
setTimeout(() => {
console.log(event.type); // 'click' ✅
}, 0);
}3.2 React 17+ 移除事件池
jsx
// React 17+ 不再使用事件池
function handleClick(event) {
setTimeout(() => {
console.log(event.type); // 'click' ✅ 无需 persist
}, 0);
}移除原因:
- 现代浏览器性能足够好,事件池优化收益小
- 容易造成困惑和 bug
- 与 Concurrent Mode 不兼容
4. 事件命名与处理
4.1 命名规范
jsx
// React 事件使用 camelCase
<button onClick={handleClick}> // ✅
<button onclick={handleClick}> // ❌ 原生 HTML 写法
// 捕获阶段加 Capture 后缀
<div onClickCapture={handleCapture}>
<button onClick={handleBubble}>Click</button>
</div>4.2 常用事件
jsx
// 鼠标事件
onClick, onDoubleClick, onMouseDown, onMouseUp
onMouseEnter, onMouseLeave // 不冒泡
onMouseOver, onMouseOut // 冒泡
// 键盘事件
onKeyDown, onKeyUp, onKeyPress (已废弃)
// 表单事件
onChange, onInput, onSubmit, onFocus, onBlur
// 触摸事件
onTouchStart, onTouchMove, onTouchEnd
// 其他
onScroll, onWheel, onCopy, onPaste4.3 onChange 的特殊性
jsx
// React 的 onChange 实际上是 onInput
// 每次输入都触发,而不是失焦时
<input onChange={e => console.log(e.target.value)} />
// 输入 'abc':打印 'a', 'ab', 'abc'
// 原生 change 事件需要用 onBlur
<input onBlur={e => console.log(e.target.value)} />5. 阻止默认行为与冒泡
5.1 preventDefault 和 stopPropagation
jsx
function Form() {
const handleSubmit = (e) => {
e.preventDefault(); // 阻止表单提交
console.log('submit');
};
const handleClick = (e) => {
e.stopPropagation(); // 阻止冒泡
console.log('click');
};
return (
<form onSubmit={handleSubmit}>
<button onClick={handleClick}>Submit</button>
</form>
);
}5.2 与原生事件混用的问题
jsx
function Component() {
const ref = useRef(null);
useEffect(() => {
// 原生事件监听
const handleNativeClick = (e) => {
console.log('native');
};
ref.current.addEventListener('click', handleNativeClick);
return () => ref.current.removeEventListener('click', handleNativeClick);
}, []);
const handleReactClick = (e) => {
e.stopPropagation(); // 只能阻止 React 合成事件的冒泡
console.log('react');
};
// 点击会打印: 'native', 'react'
// 因为原生事件先触发(直接绑定在元素上)
return <button ref={ref} onClick={handleReactClick}>Click</button>;
}5.3 阻止原生事件冒泡
jsx
function Component() {
const handleClick = (e) => {
e.stopPropagation(); // 阻止 React 事件冒泡
e.nativeEvent.stopImmediatePropagation(); // 阻止原生事件
};
return <button onClick={handleClick}>Click</button>;
}6. 事件处理最佳实践
6.1 传递参数
jsx
// 方式一:箭头函数(每次渲染创建新函数)
<button onClick={() => handleClick(id)}>Delete</button>
// 方式二:bind(每次渲染创建新函数)
<button onClick={handleClick.bind(null, id)}>Delete</button>
// 方式三:data 属性(推荐,不创建新函数)
<button data-id={id} onClick={handleClick}>Delete</button>
function handleClick(e) {
const id = e.currentTarget.dataset.id;
}
// 方式四:useCallback(需要配合 memo 才有意义)
const handleClick = useCallback(() => {
deleteItem(id);
}, [id]);6.2 事件处理函数命名
jsx
// 推荐的命名规范
const handleClick = () => {}; // handle + 事件名
const handleSubmit = () => {};
const handleInputChange = () => {}; // handle + 元素 + 事件名
// 作为 props 传递时
<Child onClick={handleClick} /> // on + 事件名
<Child onItemDelete={handleDelete} /> // on + 描述 + 动作7. 面试高频问题
Q1: React 事件和原生事件的区别?
| 区别 | React 合成事件 | 原生事件 |
|---|---|---|
| 命名 | camelCase (onClick) | 全小写 (onclick) |
| 绑定位置 | root 容器 | 元素本身 |
| 事件对象 | SyntheticEvent | Event |
| 阻止默认 | e.preventDefault() | return false 也可以 |
Q2: React 17 事件系统有什么变化?
- 事件委托从 document 改为 root 容器
- 移除事件池
- 对齐浏览器行为(onScroll 不再冒泡等)
Q3: 为什么 React 使用事件委托?
- 减少内存占用:不用给每个元素绑定监听器
- 动态元素:新增元素自动具有事件处理能力
- 统一管理:便于实现优先级调度等特性
Q4: e.stopPropagation() 能阻止原生事件吗?
不能。React 的 stopPropagation 只阻止 React 合成事件的冒泡。要阻止原生事件,需要使用 e.nativeEvent.stopImmediatePropagation()。
Q5: React 的 onChange 和原生的 change 有什么区别?
| 区别 | React onChange | 原生 change |
|---|---|---|
| 触发时机 | 每次输入都触发 | 失焦时触发 |
| 实际监听 | input 事件 | change 事件 |
| 使用场景 | 实时响应输入 | 完成输入后处理 |
Q6: 如何在 React 中使用原生事件?
jsx
useEffect(() => {
const handler = (e) => { ... };
element.addEventListener('event', handler);
return () => element.removeEventListener('event', handler);
}, []);注意:原生事件先于 React 事件触发,混用时需小心顺序问题。