React 合成事件
问题
什么是 React 合成事件?它和原生事件有什么区别?事件委托是如何实现的?
答案
合成事件(SyntheticEvent) 是 React 对浏览器原生事件的跨浏览器封装。它具有与原生事件相同的接口,但在所有浏览器中行为一致。React 使用事件委托机制,将所有事件统一绑定到根节点。
合成事件的特点
| 特性 | 合成事件 | 原生事件 |
|---|---|---|
| 绑定位置 | 根节点(root) | 目标元素 |
| 跨浏览器兼容 | ✅ 统一 API | ❌ 需要兼容处理 |
| 事件对象 | SyntheticEvent | Event |
| 事件池化 | 无 | |
| 阻止冒泡 | e.stopPropagation() | e.stopPropagation() |
事件委托机制
React 17 之前
// React 16:事件绑定在 document 上
document.addEventListener('click', reactClickHandler);
// 问题:多个 React 应用可能冲突
React 17+
// React 17+:事件绑定在 React 根容器上
const root = document.getElementById('root');
root.addEventListener('click', reactClickHandler);
合成事件与原生事件的区别
事件类型命名
// React:驼峰命名
<button onClick={handleClick}>Click</button>
<input onChange={handleChange} />
<form onSubmit={handleSubmit}>
// 原生:全小写
button.onclick = handleClick;
input.onchange = handleChange;
form.onsubmit = handleSubmit;
事件处理器
// React:传入函数
<button onClick={handleClick}>Click</button>
<button onClick={() => handleClick(id)}>Click</button>
// 原生:传入函数名字符串(不推荐)
<button onclick="handleClick()">Click</button>
阻止默认行为
// React:必须显式调用 preventDefault
function Form() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); // 阻止表单提交
// 处理逻辑
};
return <form onSubmit={handleSubmit}>...</form>;
}
// 原生:可以 return false(不推荐)
// <form onsubmit="return false;">
事件对象
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
// React 合成事件对象
console.log(e.type); // 'click'
console.log(e.target); // 目标元素
console.log(e.currentTarget); // 绑定事件的元素
console.log(e.nativeEvent); // 原生事件对象
// 访问原生事件
console.log(e.nativeEvent.offsetX);
}
SyntheticEvent API
interface SyntheticEvent<T = Element> {
// 通用属性
bubbles: boolean;
cancelable: boolean;
currentTarget: T;
defaultPrevented: boolean;
eventPhase: number;
isTrusted: boolean;
target: EventTarget;
timeStamp: number;
type: string;
// 原生事件
nativeEvent: Event;
// 方法
preventDefault(): void;
stopPropagation(): void;
isPropagationStopped(): boolean;
persist(): void; // React 16 使用,17+ 无需
}
常用事件类型
// 鼠标事件
function handleMouse(e: React.MouseEvent<HTMLDivElement>) {
console.log(e.clientX, e.clientY); // 鼠标位置
console.log(e.button); // 按键
}
// 键盘事件
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
console.log(e.key); // 按键值 'Enter', 'a'
console.log(e.keyCode); // 键码(已废弃)
console.log(e.ctrlKey); // 是否按下 Ctrl
}
// 表单事件
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}
// 焦点事件
function handleFocus(e: React.FocusEvent<HTMLInputElement>) {
console.log(e.relatedTarget); // 之前/之后聚焦的元素
}
// 剪贴板事件
function handlePaste(e: React.ClipboardEvent<HTMLInputElement>) {
console.log(e.clipboardData.getData('text'));
}
事件冒泡与捕获
冒泡阶段(默认)
function App() {
return (
<div onClick={() => console.log('div clicked')}>
<button onClick={() => console.log('button clicked')}>
Click me
</button>
</div>
);
}
// 点击按钮输出:
// button clicked
// div clicked
捕获阶段
function App() {
return (
<div onClickCapture={() => console.log('div capture')}>
<button
onClickCapture={() => console.log('button capture')}
onClick={() => console.log('button bubble')}
>
Click me
</button>
</div>
);
}
// 点击按钮输出:
// div capture(捕获阶段)
// button capture(捕获阶段)
// button bubble(冒泡阶段)
// div bubble(冒泡阶段,如果有 onClick)
阻止冒泡
function Child() {
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation(); // 阻止事件冒泡
console.log('Child clicked');
};
return <button onClick={handleClick}>Click</button>;
}
function Parent() {
return (
<div onClick={() => console.log('Parent clicked')}>
<Child />
</div>
);
}
// 点击按钮只输出:Child clicked
合成事件与原生事件混用
执行顺序
function App() {
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const button = buttonRef.current!;
// 原生事件 - 直接绑定
button.addEventListener('click', () => {
console.log('1. Native button');
});
// 原生事件 - document
document.addEventListener('click', () => {
console.log('4. Native document');
});
}, []);
return (
<div onClick={() => console.log('3. React div')}>
<button
ref={buttonRef}
onClick={() => console.log('2. React button')}
>
Click
</button>
</div>
);
}
// 点击按钮输出顺序:
// 1. Native button(原生事件捕获/目标阶段)
// 2. React button(合成事件,绑定在 root)
// 3. React div(合成事件冒泡)
// 4. Native document(原生事件冒泡到 document)
阻止原生事件冒泡
function App() {
useEffect(() => {
document.addEventListener('click', () => {
console.log('Document clicked');
});
}, []);
const handleClick = (e: React.MouseEvent) => {
// stopPropagation 只阻止 React 合成事件冒泡
e.stopPropagation();
// 要阻止原生事件,需要访问 nativeEvent
e.nativeEvent.stopImmediatePropagation();
console.log('Button clicked');
};
return <button onClick={handleClick}>Click</button>;
}
注意
e.stopPropagation() 只阻止 React 合成事件的冒泡,不影响原生事件。要同时阻止原生事件,需要使用 e.nativeEvent.stopImmediatePropagation()。
事件池化(已移除)
React 16 的事件池化
// React 16:事件对象会被复用
function handleClick(e: React.MouseEvent) {
console.log(e.type); // 'click'
setTimeout(() => {
console.log(e.type); // null(事件对象已被重置)
}, 0);
}
// 需要调用 e.persist() 保留事件对象
function handleClick(e: React.MouseEvent) {
e.persist();
setTimeout(() => {
console.log(e.type); // 'click'
}, 0);
}
React 17+ 移除事件池化
// React 17+:事件对象不再复用
function handleClick(e: React.MouseEvent) {
setTimeout(() => {
console.log(e.type); // 'click' ✅
}, 0);
}
自定义事件
使用 CustomEvent
// 触发自定义事件
function Publisher() {
const triggerEvent = () => {
const event = new CustomEvent('myCustomEvent', {
detail: { message: 'Hello from publisher' },
bubbles: true,
});
document.dispatchEvent(event);
};
return <button onClick={triggerEvent}>Publish</button>;
}
// 监听自定义事件
function Subscriber() {
const [message, setMessage] = useState('');
useEffect(() => {
const handler = (e: CustomEvent) => {
setMessage(e.detail.message);
};
document.addEventListener('myCustomEvent', handler as EventListener);
return () => {
document.removeEventListener('myCustomEvent', handler as EventListener);
};
}, []);
return <div>Message: {message}</div>;
}
使用 EventEmitter
import { EventEmitter } from 'events';
const eventBus = new EventEmitter();
function Publisher() {
const handleClick = () => {
eventBus.emit('notification', { type: 'success', message: 'Done!' });
};
return <button onClick={handleClick}>Notify</button>;
}
function Subscriber() {
const [notification, setNotification] = useState<Notification | null>(null);
useEffect(() => {
const handler = (data: Notification) => setNotification(data);
eventBus.on('notification', handler);
return () => { eventBus.off('notification', handler); };
}, []);
return notification && <div>{notification.message}</div>;
}
常见面试问题
Q1: React 事件和原生事件有什么区别?
答案:
| 区别 | React 合成事件 | 原生事件 |
|---|---|---|
| 绑定位置 | 根容器(React 17+) | 目标元素 |
| 命名 | 驼峰(onClick) | 小写(onclick) |
| 处理器 | 函数 | 函数或字符串 |
| 事件对象 | SyntheticEvent | Event |
| 跨浏览器 | ✅ 兼容 | ❌ 需处理 |
| 默认行为 | 必须 preventDefault() | 可 return false |
// React
<button onClick={(e) => { e.preventDefault(); }}>
// 原生
button.addEventListener('click', (e) => { e.preventDefault(); });
Q2: React 17 事件系统有什么变化?
答案:
- 事件绑定位置改变:从
document改为 React 根容器
// React 16
document.addEventListener('click', handler);
// React 17
rootElement.addEventListener('click', handler);
-
好处:
- 多个 React 版本可以共存
- 与其他框架更好地集成
- 微前端场景下事件不冲突
-
移除事件池化:不再需要
e.persist()
Q3: 为什么 React 使用合成事件?
答案:
- 跨浏览器兼容:统一 API,屏蔽浏览器差异
// 不同浏览器的差异被 React 处理
e.target // 统一获取目标元素
e.key // 统一获取按键
- 性能优化:事件委托减少内存占用
// 1000 个按钮只绑定一个事件
<div>
{items.map(item => (
<button onClick={handleClick}>{item}</button>
))}
</div>
- 与 React 架构集成:批量更新、优先级调度
Q4: React 中如何阻止事件冒泡到原生事件?
答案:
function Button() {
const handleClick = (e: React.MouseEvent) => {
// 阻止 React 合成事件冒泡
e.stopPropagation();
// 阻止原生事件继续传播
e.nativeEvent.stopImmediatePropagation();
};
return <button onClick={handleClick}>Click</button>;
}
| 方法 | 作用 |
|---|---|
e.stopPropagation() | 阻止 React 合成事件冒泡 |
e.nativeEvent.stopPropagation() | 阻止原生事件冒泡 |
e.nativeEvent.stopImmediatePropagation() | 阻止原生事件冒泡,且阻止同元素其他监听器 |
Q5: React 事件和原生事件的执行顺序是什么?
答案:
// 执行顺序
// 1. 原生事件捕获阶段(document → 目标)
// 2. 原生事件目标阶段(目标元素直接绑定)
// 3. 原生事件冒泡阶段(目标 → root)
// 4. React 合成事件(在 root 处理)
// 5. 原生事件继续冒泡(root → document)
记忆口诀:原生先执行,合成在 root,document 最后。