Appearance
合成事件系统
学习目标
通过本章学习,你将深入理解:
- React合成事件系统的原理
- 合成事件与原生事件的区别
- 事件池机制(React 17前)
- 事件委托的实现原理
- 捕获和冒泡阶段
- 原生事件与React事件的混用
- React 19的事件系统优化
第一部分:合成事件系统概述
1.1 什么是合成事件
合成事件(SyntheticEvent)是React对浏览器原生事件的跨浏览器包装器。
合成事件的优势
jsx
// 原生事件的问题
// 1. 浏览器兼容性问题
element.addEventListener('click', (e) => {
e.stopPropagation(); // 某些旧浏览器可能不支持
});
// 2. 事件对象属性不一致
// IE: window.event
// 现代浏览器: 参数传递
// React合成事件的优势
function SyntheticEventAdvantages() {
const handleClick = (e) => {
// 1. 跨浏览器一致的API
e.stopPropagation(); // 所有浏览器都支持
// 2. 统一的事件对象属性
console.log(e.target);
console.log(e.currentTarget);
// 3. 性能优化(事件委托)
// 4. 与React生态集成
};
return <button onClick={handleClick}>点击</button>;
}1.2 合成事件的结构
jsx
function SyntheticEventStructure() {
const handleClick = (e) => {
console.log('合成事件对象:', e);
// 合成事件的属性
console.log('bubbles:', e.bubbles);
console.log('cancelable:', e.cancelable);
console.log('currentTarget:', e.currentTarget);
console.log('defaultPrevented:', e.defaultPrevented);
console.log('eventPhase:', e.eventPhase);
console.log('isTrusted:', e.isTrusted);
console.log('nativeEvent:', e.nativeEvent); // 原生事件对象
console.log('target:', e.target);
console.log('timeStamp:', e.timeStamp);
console.log('type:', e.type);
// 方法
e.preventDefault();
e.stopPropagation();
e.persist(); // React 17+已废弃
// 访问原生事件
console.log('原生事件:', e.nativeEvent);
};
return <button onClick={handleClick}>查看合成事件</button>;
}1.3 合成事件vs原生事件
jsx
function SyntheticVsNative() {
const buttonRef = useRef(null);
useEffect(() => {
const button = buttonRef.current;
// 原生事件监听
const nativeHandler = (e) => {
console.log('原生事件:', e);
console.log('是原生Event对象:', e instanceof Event);
};
button.addEventListener('click', nativeHandler);
return () => {
button.removeEventListener('click', nativeHandler);
};
}, []);
// React合成事件
const handleClick = (e) => {
console.log('合成事件:', e);
console.log('是合成Event对象:', e instanceof Event); // false
console.log('有nativeEvent:', e.nativeEvent instanceof Event); // true
};
return <button ref={buttonRef} onClick={handleClick}>点击测试</button>;
}
// 对比总结
/*
原生事件:
- 浏览器原生Event对象
- 可能有兼容性问题
- 直接操作DOM
- 每个元素都有监听器
合成事件:
- React的SyntheticEvent对象
- 跨浏览器一致
- 通过React事件系统
- 自动事件委托
*/第二部分:事件委托机制
2.1 React的事件委托
jsx
// React 17之前:事件委托到document
// React 17+:事件委托到根容器
function EventDelegation() {
// 即使有1000个按钮
const buttons = Array(1000).fill(0).map((_, i) => i);
return (
<div>
{buttons.map(i => (
<button key={i} onClick={() => console.log(i)}>
按钮 {i}
</button>
))}
</div>
);
// React不会为每个按钮添加监听器
// 而是在根元素添加一个监听器
// 通过事件冒泡处理所有点击
}
// 原理演示
function DelegationPrinciple() {
// React内部类似这样实现:
useEffect(() => {
const root = document.getElementById('root');
const handleClick = (nativeEvent) => {
// 找到触发事件的React组件
const targetFiber = findFiberFromDOM(nativeEvent.target);
// 创建合成事件
const syntheticEvent = createSyntheticEvent(nativeEvent);
// 调用React组件的onClick处理器
if (targetFiber.props.onClick) {
targetFiber.props.onClick(syntheticEvent);
}
};
root.addEventListener('click', handleClick);
return () => {
root.removeEventListener('click', handleClick);
};
}, []);
return <div>{/* ... */}</div>;
}2.2 事件委托的好处
jsx
// 1. 性能优势
function PerformanceAdvantage() {
const [items] = useState(Array(10000).fill(0).map((_, i) => ({
id: i,
name: `Item ${i}`
})));
const handleClick = (id) => {
console.log('点击:', id);
};
// React只在root添加一个监听器
// 而不是10000个
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
// 2. 动态元素支持
function DynamicElements() {
const [items, setItems] = useState([]);
const addItem = () => {
setItems([...items, { id: Date.now(), name: 'New' }]);
};
const handleClick = (id) => {
console.log(id);
};
return (
<div>
<button onClick={addItem}>添加</button>
<ul>
{items.map(item => (
<li key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</li>
))}
</ul>
{/* 新添加的元素自动有事件处理 */}
</div>
);
}第三部分:事件冒泡和捕获
3.1 事件传播阶段
jsx
function EventPhases() {
const handleCapture = (phase) => (e) => {
console.log(`${phase} 捕获阶段`);
};
const handleBubble = (phase) => (e) => {
console.log(`${phase} 冒泡阶段`);
};
return (
<div
onClickCapture={handleCapture('外层')}
onClick={handleBubble('外层')}
>
<div
onClickCapture={handleCapture('中层')}
onClick={handleBubble('中层')}
>
<button
onClickCapture={handleCapture('内层')}
onClick={handleBubble('内层')}
>
点击测试
</button>
</div>
</div>
);
// 点击按钮,输出顺序:
// 外层 捕获阶段
// 中层 捕获阶段
// 内层 捕获阶段
// 内层 冒泡阶段
// 中层 冒泡阶段
// 外层 冒泡阶段
}3.2 阻止传播
jsx
function StopPropagationDemo() {
const handleOuter = () => console.log('外层');
const handleInner = (e) => {
e.stopPropagation();
console.log('内层');
};
return (
<div onClick={handleOuter}>
<button onClick={handleInner}>
点击(不会触发外层)
</button>
</div>
);
}
// stopPropagation详解
function StopPropagationDetails() {
const handleDocument = () => console.log('Document');
const handleOuter = () => console.log('外层Div');
const handleMiddle = () => console.log('中层Div');
const handleButton = (e) => {
e.stopPropagation();
console.log('按钮');
// 阻止事件继续向上冒泡
};
useEffect(() => {
document.addEventListener('click', handleDocument);
return () => document.removeEventListener('click', handleDocument);
}, []);
return (
<div onClick={handleOuter}>
外层
<div onClick={handleMiddle}>
中层
<button onClick={handleButton}>
点击(只输出"按钮")
</button>
</div>
</div>
);
}3.3 阻止默认行为
jsx
function PreventDefaultDemo() {
// 阻止链接跳转
const handleLinkClick = (e) => {
e.preventDefault();
console.log('链接被点击,但不会跳转');
// 自定义导航逻辑
navigateTo(e.target.href);
};
// 阻止表单提交
const handleSubmit = (e) => {
e.preventDefault();
console.log('表单提交被阻止');
// 自定义提交逻辑
submitForm();
};
// 阻止右键菜单
const handleContextMenu = (e) => {
e.preventDefault();
console.log('右键菜单被禁用');
};
return (
<div>
<a href="https://example.com" onClick={handleLinkClick}>
点击链接(不会跳转)
</a>
<form onSubmit={handleSubmit}>
<input type="text" />
<button type="submit">提交</button>
</form>
<div onContextMenu={handleContextMenu}>
右键点击这里(不会显示菜单)
</div>
</div>
);
}3.4 事件传播的实际应用
jsx
function EventPropagationApp() {
const [logs, setLogs] = useState([]);
const addLog = (message) => {
setLogs(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`]);
};
const handleOuterCapture = () => addLog('外层-捕获');
const handleOuter = () => addLog('外层-冒泡');
const handleMiddleCapture = () => addLog('中层-捕获');
const handleMiddle = () => addLog('中层-冒泡');
const handleButtonCapture = () => addLog('按钮-捕获');
const handleButton = () => addLog('按钮-冒泡');
return (
<div>
<div
style={{ padding: '30px', background: '#f0f0f0' }}
onClickCapture={handleOuterCapture}
onClick={handleOuter}
>
外层
<div
style={{ padding: '20px', background: '#ddd', margin: '10px' }}
onClickCapture={handleMiddleCapture}
onClick={handleMiddle}
>
中层
<button
style={{ margin: '10px' }}
onClickCapture={handleButtonCapture}
onClick={handleButton}
>
点击我看事件传播
</button>
</div>
</div>
<div style={{ marginTop: '20px' }}>
<h3>事件日志:</h3>
<button onClick={() => setLogs([])}>清空日志</button>
<ul>
{logs.map((log, i) => (
<li key={i}>{log}</li>
))}
</ul>
</div>
</div>
);
}第四部分:事件池机制(React 17之前)
4.1 事件池的概念
jsx
// React 16及之前:事件池机制
function EventPooling() {
const handleClick = (e) => {
console.log(e.type); // "click"
// 异步访问事件对象
setTimeout(() => {
console.log(e.type); // React 16: null (事件对象被重用)
// React 17+: "click" (不再使用事件池)
}, 0);
};
return <button onClick={handleClick}>点击</button>;
}
// React 16中需要persist()
function EventPersist() {
const handleClick = (e) => {
e.persist(); // 保持事件对象
setTimeout(() => {
console.log(e.type); // "click" (现在可以访问)
}, 0);
};
return <button onClick={handleClick}>点击</button>;
}
// React 17+:不再需要persist()
function React17Plus() {
const handleClick = (e) => {
// 不需要persist()
setTimeout(() => {
console.log(e.type); // "click" (自动保持)
}, 0);
};
return <button onClick={handleClick}>点击</button>;
}4.2 为什么移除事件池
jsx
// React 16的事件池问题
function EventPoolingProblem() {
const [events, setEvents] = useState([]);
const handleClick = (e) => {
// 错误:直接保存事件对象
setEvents([...events, e]); // 所有事件都指向同一个对象
// 正确:保存事件属性
setEvents([...events, {
type: e.type,
target: e.target,
timeStamp: e.timeStamp
}]);
};
return (
<div>
<button onClick={handleClick}>点击</button>
<ul>
{events.map((event, i) => (
<li key={i}>{event.type} at {event.timeStamp}</li>
))}
</ul>
</div>
);
}
// React 17+:可以直接保存事件对象
function React17EventSaving() {
const [events, setEvents] = useState([]);
const handleClick = (e) => {
// 现在可以直接保存
setEvents([...events, e]);
};
return (
<div>
<button onClick={handleClick}>点击</button>
<ul>
{events.map((event, i) => (
<li key={i}>{event.type} at {event.timeStamp}</li>
))}
</ul>
</div>
);
}第五部分:React 17的事件系统变化
5.1 事件委托位置的变化
jsx
// React 16及之前:事件附加到document
// <html>
// <body>
// <div id="root">
// <App />
// </div>
// </body>
// </html>
// 所有React事件监听器都附加到document
// React 17+:事件附加到根容器
// <html>
// <body>
// <div id="root"> ← 事件监听器附加在这里
// <App />
// </div>
// </body>
// </html>
// 这个变化的好处
function React17Benefits() {
// 1. 更容易嵌入React到其他框架
// 2. 避免与其他DOM事件冲突
// 3. 支持多个React根
return (
<div>
{/* 两个独立的React应用 */}
<div id="app1">
<ReactApp1 />
</div>
<div id="app2">
<ReactApp2 />
</div>
</div>
);
}5.2 与原生事件的交互
jsx
function NativeEventInteraction() {
const buttonRef = useRef(null);
const [logs, setLogs] = useState([]);
const addLog = (msg) => {
setLogs(prev => [...prev, msg]);
};
useEffect(() => {
const button = buttonRef.current;
// 原生事件(冒泡阶段)
const nativeHandler = () => {
addLog('原生事件-冒泡');
};
// 原生事件(捕获阶段)
const nativeCaptureHandler = () => {
addLog('原生事件-捕获');
};
button.addEventListener('click', nativeHandler);
button.addEventListener('click', nativeCaptureHandler, true);
// document级别的原生事件
const documentHandler = () => {
addLog('Document原生事件');
};
document.addEventListener('click', documentHandler);
return () => {
button.removeEventListener('click', nativeHandler);
button.removeEventListener('click', nativeCaptureHandler, true);
document.removeEventListener('click', documentHandler);
};
}, []);
// React合成事件
const handleReactClick = () => {
addLog('React合成事件-冒泡');
};
const handleReactClickCapture = () => {
addLog('React合成事件-捕获');
};
return (
<div>
<button
ref={buttonRef}
onClick={handleReactClick}
onClickCapture={handleReactClickCapture}
>
点击测试事件顺序
</button>
<div>
<h3>事件触发顺序:</h3>
<button onClick={() => setLogs([])}>清空</button>
<ol>
{logs.map((log, i) => (
<li key={i}>{log}</li>
))}
</ol>
</div>
{/* React 17+的执行顺序:
1. 原生事件-捕获(从document到目标)
2. React合成事件-捕获
3. 原生事件-冒泡(在目标元素)
4. React合成事件-冒泡
5. Document原生事件
*/}
</div>
);
}5.3 阻止传播的影响
jsx
function StopPropagationImpact() {
const buttonRef = useRef(null);
useEffect(() => {
// 原生事件监听器
const handleNative = () => {
console.log('原生事件被触发');
};
document.addEventListener('click', handleNative);
return () => {
document.removeEventListener('click', handleNative);
};
}, []);
// React事件中stopPropagation
const handleReactClick = (e) => {
e.stopPropagation();
console.log('React事件');
};
return (
<button ref={buttonRef} onClick={handleReactClick}>
点击
</button>
);
// React 16:
// - React事件的stopPropagation会阻止document的原生事件
// React 17+:
// - React事件的stopPropagation只阻止React事件传播
// - 不会影响原生事件(因为事件附加在root,不是document)
}第六部分:常见事件类型详解
6.1 鼠标事件
jsx
function MouseEvents() {
const [info, setInfo] = useState('');
const handleMouseEvent = (type) => (e) => {
setInfo(`${type}:
位置(${e.clientX}, ${e.clientY})
按钮: ${e.button}
Alt: ${e.altKey}
Ctrl: ${e.ctrlKey}
Shift: ${e.shiftKey}
Meta: ${e.metaKey}`);
};
return (
<div style={{ padding: '20px', background: '#f0f0f0' }}>
<div
style={{ padding: '50px', background: 'white', cursor: 'pointer' }}
onClick={handleMouseEvent('onClick')}
onDoubleClick={handleMouseEvent('onDoubleClick')}
onMouseDown={handleMouseEvent('onMouseDown')}
onMouseUp={handleMouseEvent('onMouseUp')}
onMouseEnter={handleMouseEvent('onMouseEnter')}
onMouseLeave={handleMouseEvent('onMouseLeave')}
onMouseMove={handleMouseEvent('onMouseMove')}
onMouseOver={handleMouseEvent('onMouseOver')}
onMouseOut={handleMouseEvent('onMouseOut')}
onContextMenu={handleMouseEvent('onContextMenu')}
>
在这里测试各种鼠标事件
</div>
<pre>{info}</pre>
</div>
);
}
// 鼠标事件的特定应用
function MouseEventApplications() {
// 1. 拖拽功能
const [position, setPosition] = useState({ x: 0, y: 0 });
const [dragging, setDragging] = useState(false);
const [offset, setOffset] = useState({ x: 0, y: 0 });
const handleMouseDown = (e) => {
setDragging(true);
setOffset({
x: e.clientX - position.x,
y: e.clientY - position.y
});
};
const handleMouseMove = (e) => {
if (dragging) {
setPosition({
x: e.clientX - offset.x,
y: e.clientY - offset.y
});
}
};
const handleMouseUp = () => {
setDragging(false);
};
return (
<div
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
style={{ height: '400px', position: 'relative', background: '#f0f0f0' }}
>
<div
onMouseDown={handleMouseDown}
style={{
position: 'absolute',
left: `${position.x}px`,
top: `${position.y}px`,
width: '100px',
height: '100px',
background: 'blue',
cursor: dragging ? 'grabbing' : 'grab',
userSelect: 'none'
}}
>
拖动我
</div>
</div>
);
}6.2 键盘事件
jsx
function KeyboardEvents() {
const [keys, setKeys] = useState([]);
const handleKeyDown = (e) => {
const keyInfo = {
key: e.key,
code: e.code,
keyCode: e.keyCode, // 已废弃但仍然可用
altKey: e.altKey,
ctrlKey: e.ctrlKey,
shiftKey: e.shiftKey,
metaKey: e.metaKey
};
setKeys(prev => [...prev, keyInfo]);
};
// 快捷键处理
const handleShortcut = (e) => {
// Ctrl+S: 保存
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
console.log('保存快捷键');
}
// Ctrl+Z: 撤销
if (e.ctrlKey && e.key === 'z') {
e.preventDefault();
console.log('撤销快捷键');
}
// Esc: 取消
if (e.key === 'Escape') {
console.log('ESC键');
}
// Enter: 确认
if (e.key === 'Enter') {
console.log('Enter键');
}
};
return (
<div>
<input
type="text"
onKeyDown={handleKeyDown}
onKeyUp={handleShortcut}
placeholder="输入按键测试"
/>
<div>
<h3>按键记录:</h3>
<ul>
{keys.map((key, i) => (
<li key={i}>
{key.key} (code: {key.code})
{key.ctrlKey && ' + Ctrl'}
{key.altKey && ' + Alt'}
{key.shiftKey && ' + Shift'}
</li>
))}
</ul>
</div>
</div>
);
}
// 键盘导航
function KeyboardNavigation() {
const [selectedIndex, setSelectedIndex] = useState(0);
const items = ['项目1', '项目2', '项目3', '项目4', '项目5'];
const handleKeyDown = (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex(i => Math.min(i + 1, items.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(i => Math.max(i - 1, 0));
break;
case 'Home':
e.preventDefault();
setSelectedIndex(0);
break;
case 'End':
e.preventDefault();
setSelectedIndex(items.length - 1);
break;
case 'Enter':
console.log('选择:', items[selectedIndex]);
break;
}
};
return (
<div tabIndex={0} onKeyDown={handleKeyDown} style={{ outline: 'none' }}>
<p>使用方向键导航,Enter选择</p>
<ul>
{items.map((item, i) => (
<li
key={i}
style={{
background: i === selectedIndex ? 'lightblue' : 'white',
padding: '10px',
cursor: 'pointer'
}}
onClick={() => setSelectedIndex(i)}
>
{item}
</li>
))}
</ul>
</div>
);
}6.3 表单事件
jsx
function FormEvents() {
const [formData, setFormData] = useState({
text: '',
textarea: '',
select: '',
checkbox: false,
radio: ''
});
// onChange: 值变化时
const handleChange = (field) => (e) => {
const value = e.target.type === 'checkbox'
? e.target.checked
: e.target.value;
setFormData({ ...formData, [field]: value });
};
// onInput: 输入时(更频繁)
const handleInput = (e) => {
console.log('输入中:', e.target.value);
};
// onFocus: 获得焦点
const handleFocus = (e) => {
console.log('焦点:', e.target.name);
};
// onBlur: 失去焦点
const handleBlur = (e) => {
console.log('失焦:', e.target.name);
};
// onSubmit: 表单提交
const handleSubmit = (e) => {
e.preventDefault();
console.log('提交:', formData);
};
return (
<form onSubmit={handleSubmit}>
<input
name="text"
value={formData.text}
onChange={handleChange('text')}
onInput={handleInput}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder="文本输入"
/>
<textarea
name="textarea"
value={formData.textarea}
onChange={handleChange('textarea')}
placeholder="多行文本"
/>
<select
name="select"
value={formData.select}
onChange={handleChange('select')}
>
<option value="">选择...</option>
<option value="a">选项A</option>
<option value="b">选项B</option>
</select>
<label>
<input
type="checkbox"
checked={formData.checkbox}
onChange={handleChange('checkbox')}
/>
复选框
</label>
<button type="submit">提交</button>
</form>
);
}6.4 触摸事件
jsx
function TouchEvents() {
const [touches, setTouches] = useState([]);
const handleTouchStart = (e) => {
const touchList = Array.from(e.touches).map(touch => ({
id: touch.identifier,
x: touch.clientX,
y: touch.clientY
}));
setTouches(touchList);
};
const handleTouchMove = (e) => {
e.preventDefault(); // 防止页面滚动
const touchList = Array.from(e.touches).map(touch => ({
id: touch.identifier,
x: touch.clientX,
y: touch.clientY
}));
setTouches(touchList);
};
const handleTouchEnd = () => {
setTouches([]);
};
return (
<div
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{
width: '100%',
height: '300px',
background: '#f0f0f0',
position: 'relative',
touchAction: 'none' // 禁用默认触摸行为
}}
>
<p>触摸这里</p>
{touches.map(touch => (
<div
key={touch.id}
style={{
position: 'absolute',
left: touch.x,
top: touch.y,
width: '50px',
height: '50px',
borderRadius: '50%',
background: 'blue',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none'
}}
/>
))}
</div>
);
}
// 滑动手势
function SwipeGesture() {
const [startX, setStartX] = useState(0);
const [direction, setDirection] = useState('');
const handleTouchStart = (e) => {
setStartX(e.touches[0].clientX);
};
const handleTouchEnd = (e) => {
const endX = e.changedTouches[0].clientX;
const diff = endX - startX;
if (Math.abs(diff) > 50) { // 最小滑动距离
if (diff > 0) {
setDirection('向右滑动');
} else {
setDirection('向左滑动');
}
}
};
return (
<div
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
style={{
padding: '50px',
background: '#e0e0e0',
textAlign: 'center'
}}
>
<p>在这里滑动</p>
<p>{direction}</p>
</div>
);
}第七部分:性能优化
7.1 事件处理器优化
jsx
// 不好:每次渲染创建新函数
function BadEventHandler() {
const [count, setCount] = useState(0);
return (
<div>
{Array(1000).fill(0).map((_, i) => (
<button key={i} onClick={() => console.log(i)}>
按钮 {i}
</button>
))}
</div>
);
}
// 好:使用useCallback
function GoodEventHandler() {
const [count, setCount] = useState(0);
const handleClick = useCallback((i) => {
console.log(i);
}, []);
return (
<div>
{Array(1000).fill(0).map((_, i) => (
<Button key={i} index={i} onClick={handleClick} />
))}
</div>
);
}
const Button = React.memo(({ index, onClick }) => {
return (
<button onClick={() => onClick(index)}>
按钮 {index}
</button>
);
});7.2 事件委托优化
jsx
// 使用事件委托处理大列表
function EventDelegationOptimization() {
const [items] = useState(
Array(10000).fill(0).map((_, i) => ({ id: i, name: `Item ${i}` }))
);
// 在父元素统一处理点击
const handleListClick = (e) => {
const target = e.target.closest('[data-id]');
if (target) {
const id = parseInt(target.dataset.id);
console.log('点击项:', id);
}
};
return (
<ul onClick={handleListClick}>
{items.map(item => (
<li key={item.id} data-id={item.id}>
{item.name}
</li>
))}
</ul>
);
}7.3 防抖和节流
jsx
// 防抖:延迟执行
function DebounceExample() {
const [value, setValue] = useState('');
const [debouncedValue, setDebouncedValue] = useState('');
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, 500);
return () => clearTimeout(timer);
}, [value]);
return (
<div>
<input
value={value}
onChange={e => setValue(e.target.value)}
placeholder="输入搜索..."
/>
<p>实时值: {value}</p>
<p>防抖值: {debouncedValue}</p>
</div>
);
}
// 节流:限制执行频率
function ThrottleExample() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const lastUpdate = useRef(0);
const handleMouseMove = (e) => {
const now = Date.now();
// 每100ms最多更新一次
if (now - lastUpdate.current >= 100) {
setPosition({ x: e.clientX, y: e.clientY });
lastUpdate.current = now;
}
};
return (
<div
onMouseMove={handleMouseMove}
style={{ height: '200px', background: '#f0f0f0' }}
>
<p>鼠标位置: ({position.x}, {position.y})</p>
</div>
);
}第八部分:React 19的事件系统增强
8.1 自动批处理与事件
jsx
// React 19中,所有事件处理器中的状态更新都会自动批处理
function React19AutoBatching() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const [text, setText] = useState('');
console.log('渲染'); // 只渲染一次
const handleClick = () => {
setCount(c => c + 1);
setFlag(f => !f);
setText('updated');
// 三个状态更新批处理,只触发一次渲染
};
// 甚至在异步回调中也会批处理
const handleAsyncClick = async () => {
await fetchData();
setCount(c => c + 1);
setFlag(f => !f);
setText('async');
// React 19: 仍然批处理
// React 18之前: 会触发三次渲染
};
return (
<div>
<button onClick={handleClick}>同步更新</button>
<button onClick={handleAsyncClick}>异步更新</button>
<p>计数: {count}, 标志: {String(flag)}, 文本: {text}</p>
</div>
);
}8.2 新的事件属性
jsx
function React19NewEventProps() {
const handlePointerEvent = (e) => {
// React 19增强了指针事件支持
console.log({
pointerType: e.pointerType, // 'mouse', 'pen', 'touch'
pressure: e.pressure, // 压力值
tangentialPressure: e.tangentialPressure,
tiltX: e.tiltX,
tiltY: e.tiltY,
twist: e.twist,
width: e.width,
height: e.height
});
};
return (
<div
onPointerDown={handlePointerEvent}
onPointerMove={handlePointerEvent}
onPointerUp={handlePointerEvent}
style={{ width: '300px', height: '300px', background: '#f0f0f0' }}
>
测试指针事件
</div>
);
}第九部分:最佳实践
9.1 参数传递选择
jsx
// 小列表:箭头函数
<button onClick={() => handleClick(id)}>点击</button>
// 大列表:事件委托 + data属性
<ul onClick={handleListClick}>
{items.map(item => (
<li data-id={item.id}>{item.name}</li>
))}
</ul>
// 性能敏感:useCallback + 提取组件
const MemoItem = React.memo(({ item, onClick }) => (
<li onClick={onClick}>{item.name}</li>
));
const handleClick = useCallback((id) => {}, []);9.2 避免内联对象和数组
jsx
// 不好:每次渲染创建新对象
function BadInlineObject() {
return (
<Child
config={{ theme: 'dark', lang: 'zh' }} // 每次都是新对象
items={[1, 2, 3]} // 每次都是新数组
/>
);
}
// 好:使用useMemo或提取到组件外
const CONFIG = { theme: 'dark', lang: 'zh' };
const ITEMS = [1, 2, 3];
function GoodStableRefs() {
return (
<Child
config={CONFIG}
items={ITEMS}
/>
);
}
// 或使用useMemo(如果需要动态值)
function WithMemo({ theme }) {
const config = useMemo(() => ({
theme,
lang: 'zh'
}), [theme]);
return <Child config={config} />;
}9.3 事件命名规范
jsx
function EventNaming() {
// 推荐的命名规范:
// handleXxx: 事件处理函数
// onXxx: 回调Props
const handleClick = () => {
console.log('点击');
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('提交');
};
const handleChange = (e) => {
console.log('变化:', e.target.value);
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button onClick={handleClick}>提交</button>
</form>
);
}
// 组件Props命名
function MyButton({ onClick, onHover, onFocus }) {
return (
<button
onClick={onClick}
onMouseEnter={onHover}
onFocus={onFocus}
>
按钮
</button>
);
}9.4 错误处理
jsx
function ErrorHandling() {
const [error, setError] = useState(null);
const handleClick = async () => {
try {
await fetchData();
setError(null);
} catch (err) {
setError(err.message);
console.error('错误:', err);
}
};
const handleInputChange = (e) => {
try {
const value = e.target.value;
if (!value.match(/^[0-9]+$/)) {
throw new Error('只能输入数字');
}
// 处理有效输入
} catch (err) {
setError(err.message);
}
};
return (
<div>
{error && <div className="error">{error}</div>}
<input onChange={handleInputChange} />
<button onClick={handleClick}>提交</button>
</div>
);
}第十部分:调试技巧
10.1 事件日志记录
jsx
function EventLogging() {
const logEvent = (eventName) => (e) => {
console.group(`🎯 ${eventName}`);
console.log('事件类型:', e.type);
console.log('目标元素:', e.target);
console.log('当前目标:', e.currentTarget);
console.log('事件阶段:', e.eventPhase);
console.log('时间戳:', e.timeStamp);
console.log('是否冒泡:', e.bubbles);
console.log('是否可取消:', e.cancelable);
console.log('原生事件:', e.nativeEvent);
console.groupEnd();
};
return (
<div onClick={logEvent('Outer Click')}>
<button onClick={logEvent('Button Click')}>
点击查看事件详情
</button>
</div>
);
}10.2 事件追踪工具
jsx
function EventTracker() {
const [eventLog, setEventLog] = useState([]);
const trackEvent = (name) => (e) => {
const eventInfo = {
name,
type: e.type,
target: e.target.tagName,
timeStamp: e.timeStamp,
phase: e.eventPhase
};
setEventLog(prev => [...prev, eventInfo]);
};
return (
<div>
<div
onClick={trackEvent('外层')}
onClickCapture={trackEvent('外层捕获')}
style={{ padding: '20px', background: '#f0f0f0' }}
>
外层
<button
onClick={trackEvent('按钮')}
onClickCapture={trackEvent('按钮捕获')}
>
点击
</button>
</div>
<div>
<h3>事件日志:</h3>
<button onClick={() => setEventLog([])}>清空</button>
<table>
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>目标</th>
<th>阶段</th>
<th>时间</th>
</tr>
</thead>
<tbody>
{eventLog.map((event, i) => (
<tr key={i}>
<td>{event.name}</td>
<td>{event.type}</td>
<td>{event.target}</td>
<td>{event.phase === 1 ? '捕获' : '冒泡'}</td>
<td>{Math.round(event.timeStamp)}ms</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}10.3 性能分析
jsx
import { Profiler } from 'react';
function PerformanceAnalysis() {
const [items] = useState(Array(1000).fill(0).map((_, i) => ({
id: i,
name: `Item ${i}`
})));
const handleClick = (id) => {
console.log('点击:', id);
};
const onRenderCallback = (
id,
phase,
actualDuration,
baseDuration
) => {
console.log(`${id} ${phase} 阶段耗时: ${actualDuration}ms`);
};
return (
<Profiler id="EventList" onRender={onRenderCallback}>
<ul>
{items.map(item => (
<li key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</li>
))}
</ul>
</Profiler>
);
}第十一部分:常见问题与解决
11.1 问题1:事件处理器不触发
jsx
// 问题:忘记绑定事件
function Problem1() {
const handleClick = () => {
console.log('点击');
};
return <button>点击</button>; // 忘记 onClick={handleClick}
}
// 解决
function Solution1() {
const handleClick = () => {
console.log('点击');
};
return <button onClick={handleClick}>点击</button>;
}11.2 问题2:this指向错误(类组件)
jsx
// 问题:类组件中this丢失
class Problem2 extends React.Component {
handleClick() {
console.log(this.state); // this is undefined
}
render() {
return <button onClick={this.handleClick}>点击</button>;
}
}
// 解决方案1:箭头函数
class Solution2a extends React.Component {
handleClick = () => {
console.log(this.state); // 正确
}
render() {
return <button onClick={this.handleClick}>点击</button>;
}
}
// 解决方案2:bind
class Solution2b extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this.state);
}
render() {
return <button onClick={this.handleClick}>点击</button>;
}
}
// 解决方案3:使用函数组件(推荐)
function Solution2c() {
const handleClick = () => {
console.log('点击');
};
return <button onClick={handleClick}>点击</button>;
}11.3 问题3:无法访问事件对象属性
jsx
// 问题:React 16中异步访问事件
function Problem3() {
const handleClick = (e) => {
setTimeout(() => {
console.log(e.target); // React 16: null
}, 0);
};
return <button onClick={handleClick}>点击</button>;
}
// 解决方案1:React 16中使用persist()
function Solution3a() {
const handleClick = (e) => {
e.persist();
setTimeout(() => {
console.log(e.target); // 正确
}, 0);
};
return <button onClick={handleClick}>点击</button>;
}
// 解决方案2:保存需要的属性
function Solution3b() {
const handleClick = (e) => {
const target = e.target;
setTimeout(() => {
console.log(target); // 正确
}, 0);
};
return <button onClick={handleClick}>点击</button>;
}
// 解决方案3:升级到React 17+(推荐)
function Solution3c() {
const handleClick = (e) => {
setTimeout(() => {
console.log(e.target); // React 17+: 正确
}, 0);
};
return <button onClick={handleClick}>点击</button>;
}11.4 问题4:stopPropagation不生效
jsx
// 问题:在原生事件中阻止不了React事件
function Problem4() {
const buttonRef = useRef(null);
useEffect(() => {
const button = buttonRef.current;
button.addEventListener('click', (e) => {
e.stopPropagation(); // 只阻止原生事件传播
});
}, []);
const handleReactClick = () => {
console.log('React事件仍然触发'); // 仍然会执行
};
return <button ref={buttonRef} onClick={handleReactClick}>点击</button>;
}
// 解决:使用React事件或在捕获阶段阻止
function Solution4() {
const handleReactClick = (e) => {
e.stopPropagation(); // 使用React事件阻止
};
return <button onClick={handleReactClick}>点击</button>;
}第十二部分:实战综合案例
12.1 拖拽排序列表
jsx
function DraggableList() {
const [items, setItems] = useState([
{ id: 1, text: '项目1' },
{ id: 2, text: '项目2' },
{ id: 3, text: '项目3' }
]);
const [draggingId, setDraggingId] = useState(null);
const [overIndex, setOverIndex] = useState(null);
const handleDragStart = (id) => (e) => {
setDraggingId(id);
e.dataTransfer.effectAllowed = 'move';
};
const handleDragOver = (index) => (e) => {
e.preventDefault();
setOverIndex(index);
};
const handleDrop = (dropIndex) => (e) => {
e.preventDefault();
if (draggingId === null) return;
const dragIndex = items.findIndex(item => item.id === draggingId);
const newItems = [...items];
const [draggedItem] = newItems.splice(dragIndex, 1);
newItems.splice(dropIndex, 0, draggedItem);
setItems(newItems);
setDraggingId(null);
setOverIndex(null);
};
const handleDragEnd = () => {
setDraggingId(null);
setOverIndex(null);
};
return (
<ul>
{items.map((item, index) => (
<li
key={item.id}
draggable
onDragStart={handleDragStart(item.id)}
onDragOver={handleDragOver(index)}
onDrop={handleDrop(index)}
onDragEnd={handleDragEnd}
style={{
padding: '10px',
margin: '5px',
background: draggingId === item.id ? '#e0e0e0' :
overIndex === index ? '#f0f0f0' : 'white',
border: '1px solid #ccc',
cursor: 'move'
}}
>
{item.text}
</li>
))}
</ul>
);
}12.2 图片裁剪工具
jsx
function ImageCropper({ imageSrc }) {
const [crop, setCrop] = useState({ x: 0, y: 0, width: 100, height: 100 });
const [dragging, setDragging] = useState(false);
const [resizing, setResizing] = useState(false);
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const handleMouseDown = (type) => (e) => {
if (type === 'move') {
setDragging(true);
} else {
setResizing(true);
}
setStartPos({ x: e.clientX, y: e.clientY });
};
const handleMouseMove = (e) => {
if (dragging) {
const dx = e.clientX - startPos.x;
const dy = e.clientY - startPos.y;
setCrop(prev => ({
...prev,
x: prev.x + dx,
y: prev.y + dy
}));
setStartPos({ x: e.clientX, y: e.clientY });
} else if (resizing) {
const dx = e.clientX - startPos.x;
const dy = e.clientY - startPos.y;
setCrop(prev => ({
...prev,
width: Math.max(50, prev.width + dx),
height: Math.max(50, prev.height + dy)
}));
setStartPos({ x: e.clientX, y: e.clientY });
}
};
const handleMouseUp = () => {
setDragging(false);
setResizing(false);
};
return (
<div
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
style={{ position: 'relative', width: '600px', height: '400px' }}
>
<img src={imageSrc} alt="裁剪" style={{ width: '100%', height: '100%' }} />
<div
style={{
position: 'absolute',
left: crop.x,
top: crop.y,
width: crop.width,
height: crop.height,
border: '2px dashed white',
cursor: 'move'
}}
onMouseDown={handleMouseDown('move')}
>
<div
style={{
position: 'absolute',
right: -5,
bottom: -5,
width: 10,
height: 10,
background: 'white',
border: '1px solid black',
cursor: 'nwse-resize'
}}
onMouseDown={handleMouseDown('resize')}
/>
</div>
</div>
);
}练习题
基础练习
- 创建组件显示事件对象的各个属性
- 实现参数传递的三种方式
- 测试事件的捕获和冒泡
进阶练习
- 实现一个图片裁剪工具
- 创建键盘导航的下拉菜单
- 实现拖拽排序列表
高级练习
- 分析React事件系统的源码
- 实现自定义的事件系统
- 优化大列表的事件处理性能
通过本章学习,你已经深入理解了React的事件对象和参数传递机制。这些知识是构建复杂交互的基础。继续学习,掌握更多React技能!