事件处理
事件处理是 React 应用中最常见的交互方式之一。React 实现了一套合成事件系统,它提供了跨浏览器的一致接口,让你无需关心不同浏览器之间的差异。
事件处理的基本概念
在 React 中,事件处理与传统 DOM 事件处理有几个重要区别:
命名约定
React 事件使用驼峰命名法(camelCase),而不是全小写。这是 React 与原生 DOM 事件最直观的区别。
// 原生 DOM 事件(全小写)
<button onclick="handleClick()">点击</button>
// React 事件(驼峰命名)
<button onClick={handleClick}>点击</button>
传递函数而非字符串
在原生 DOM 中,事件属性接收一个字符串形式的 JavaScript 代码。而在 React 中,你直接传递一个函数引用。
// 原生 DOM:传递字符串
<button onclick="alert('clicked')">点击</button>
// React:传递函数
<button onClick={() => alert('clicked')}>点击</button>
这种设计让事件处理更加类型安全,也更容易在 TypeScript 中获得类型提示。
基础事件绑定
直接传递函数引用
最简单的方式是直接将函数引用传递给事件处理器。
function ClickButton() { function handleClick() { alert('按钮被点击了!'); } return ( <button onClick={handleClick} style={{ padding: '10px 20px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px' }} > 点击我 </button> ); }
内联箭头函数
对于简单的逻辑,可以直接使用内联箭头函数。
function InlineHandler() { const [count, setCount] = useState(0); return ( <div style={{ textAlign: 'center' }}> <p>当前计数: <strong>{count}</strong></p> <button onClick={() => setCount(count + 1)}> 增加 </button> <button onClick={() => setCount(0)} style={{ marginLeft: '10px' }}> 重置 </button> </div> ); }
传递参数给事件处理器
当需要传递额外参数时,可以使用箭头函数包装。
function ItemList() { const handleDelete = (id, itemName) => { alert(`删除项目: ${itemName} (ID: ${id})`); }; const items = [ { id: 1, name: '苹果' }, { id: 2, name: '香蕉' }, { id: 3, name: '橙子' }, ]; return ( <ul style={{ listStyle: 'none', padding: 0 }}> {items.map(item => ( <li key={item.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 12px', marginBottom: '4px', background: '#f1f5f9', borderRadius: '4px' }}> <span>{item.name}</span> <button onClick={() => handleDelete(item.id, item.name)} style={{ padding: '4px 8px', background: '#ef4444', color: 'white', border: 'none', borderRadius: '4px' }} > 删除 </button> </li> ))} </ul> ); }
另一种方式是使用 bind 方法,但箭头函数更符合现代 React 开发习惯。
// 使用 bind(较少使用)
<button onClick={handleDelete.bind(null, item.id)}>删除</button>
// 使用箭头函数(推荐)
<button onClick={() => handleDelete(item.id)}>删除</button>
合成事件对象
React 实现了合成事件系统(SyntheticEvent),它是对浏览器原生事件的跨浏览器封装。合成事件具有与原生事件相同的接口,包括 stopPropagation() 和 preventDefault(),但在所有浏览器中表现一致。
访问事件对象
事件处理器会自动接收事件对象作为第一个参数。
function MouseEventDemo() { const handleMouseMove = (event) => { // event 是合成事件对象 console.log('鼠标位置:', event.clientX, event.clientY); }; const handleClick = (event) => { console.log('点击目标:', event.target.tagName); console.log('原生事件:', event.nativeEvent); }; return ( <div onClick={handleClick} onMouseMove={handleMouseMove} style={{ padding: '20px', background: '#f0f9ff', border: '2px dashed #3b82f6', borderRadius: '8px', cursor: 'pointer' }} > <p>在这个区域移动鼠标,查看控制台输出</p> <p>点击查看事件目标信息</p> </div> ); }
合成事件的主要属性
| 属性 | 说明 |
|---|---|
bubbles | 事件是否冒泡 |
cancelable | 事件是否可取消 |
currentTarget | 绑定事件处理器的元素 |
target | 触发事件的元素 |
nativeEvent | 原生浏览器事件对象 |
preventDefault() | 阻止默认行为 |
stopPropagation() | 阻止事件冒泡 |
isDefaultPrevented() | 是否已阻止默认行为 |
isPropagationStopped() | 是否已阻止冒泡 |
currentTarget 与 target 的区别
这是一个重要的概念:target 指向实际触发事件的元素,而 currentTarget 指向绑定事件处理器的元素。
function TargetDemo() { const handleClick = (event) => { alert(`target: ${event.target.tagName}\ncurrentTarget: ${event.currentTarget.tagName}`); }; return ( <div onClick={handleClick} style={{ padding: '20px', background: '#fee2e2' }} > <span style={{ background: '#fecaca', padding: '10px' }}> <strong style={{ color: '#dc2626' }}>点击这段文字</strong> </span> <p style={{ fontSize: '12px', color: '#666' }}>点击不同层级,观察 target 和 currentTarget 的区别</p> </div> ); }
当点击 <strong> 元素时:
event.target是<strong>event.currentTarget是<div>
这个区别在事件委托中非常重要。
阻止默认行为与事件冒泡
阻止默认行为
在原生 DOM 中,可以通过返回 false 来阻止某些事件的默认行为。但在 React 中,必须显式调用 preventDefault()。
function FormSubmitDemo() { const handleSubmit = (event) => { event.preventDefault(); // 阻止表单默认提交行为 alert('表单提交被阻止,页面不会刷新'); }; const handleLinkClick = (event) => { event.preventDefault(); // 阻止链接跳转 alert('链接跳转被阻止'); }; return ( <div> <form onSubmit={handleSubmit} style={{ marginBottom: '10px' }}> <input type="text" placeholder="输入内容..." style={{ padding: '8px' }} /> <button type="submit" style={{ marginLeft: '10px', padding: '8px 16px' }}>提交表单</button> </form> <a href="https://react.dev" onClick={handleLinkClick} style={{ color: '#3b82f6' }}> 点击这个链接(跳转被阻止) </a> </div> ); }
常见需要阻止默认行为的场景:
- 表单提交(阻止页面刷新)
- 链接跳转(阻止页面导航)
- 右键菜单(阻止显示默认菜单)
- 文本选择(阻止选中文字)
阻止事件冒泡
使用 stopPropagation() 可以阻止事件继续向上传播。
function PropagationDemo() { const [logs, setLogs] = useState([]); const addLog = (message) => { setLogs(prev => [...prev.slice(-4), message]); }; return ( <div onClick={() => addLog('外层 div 被点击')} style={{ padding: '20px', background: '#e0e7ff', borderRadius: '8px' }} > <p style={{ margin: '0 0 10px' }}>点击不同按钮观察事件传播:</p> <button onClick={(e) => { e.stopPropagation(); addLog('按钮1:冒泡被阻止'); }} style={{ padding: '10px', background: '#ef4444', color: 'white', border: 'none', borderRadius: '4px' }} > 阻止冒泡 </button> <button onClick={() => addLog('按钮2:正常冒泡')} style={{ marginLeft: '10px', padding: '10px', background: '#22c55e', color: 'white', border: 'none', borderRadius: '4px' }} > 允许冒泡 </button> <div style={{ marginTop: '10px', padding: '10px', background: 'white', borderRadius: '4px', minHeight: '60px' }}> <strong>事件日志:</strong> {logs.map((log, i) => <p key={i} style={{ margin: '4px 0', fontSize: '14px' }}>{log}</p>)} </div> </div> ); }
事件传播的三个阶段
DOM 事件传播经历三个阶段:捕获阶段、目标阶段和冒泡阶段。React 的事件系统主要在冒泡阶段处理事件。
捕获阶段(Capture Phase)
↓ 从 document 向下传播到目标元素
目标阶段(Target Phase)
↓ 到达目标元素
冒泡阶段(Bubble Phase)
↑ 从目标元素向上传播回 document
如果需要在捕获阶段处理事件,可以在事件名后添加 Capture 后缀:
// 冒泡阶段处理(默认)
<div onClick={handleClick}>
// 捕获阶段处理
<div onClickCapture={handleClick}>
常见事件类型
鼠标事件
鼠标事件是最常用的交互事件类型。
| 事件名 | 触发时机 |
|---|---|
onClick | 单击 |
onDoubleClick | 双击 |
onMouseDown | 鼠标按下 |
onMouseUp | 鼠标释放 |
onMouseEnter | 鼠标进入(不冒泡) |
onMouseLeave | 鼠标离开(不冒泡) |
onMouseMove | 鼠标移动 |
onMouseOver | 鼠标悬停(冒泡) |
onMouseOut | 鼠标移出(冒泡) |
onContextMenu | 右键菜单 |
function MouseEventsDemo() { const [position, setPosition] = useState({ x: 0, y: 0 }); const [isHovering, setIsHovering] = useState(false); return ( <div onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })} onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} style={{ padding: '30px', background: isHovering ? '#dbeafe' : '#f8fafc', border: '2px solid ' + (isHovering ? '#3b82f6' : '#e2e8f0'), borderRadius: '8px', transition: 'all 0.2s', cursor: 'crosshair' }} > <p>鼠标位置: X={position.x}, Y={position.y}</p> <p>状态: {isHovering ? '鼠标在区域内' : '鼠标在区域外'}</p> </div> ); }
键盘事件
键盘事件用于处理用户的键盘输入。
| 事件名 | 触发时机 |
|---|---|
onKeyDown | 按键按下 |
onKeyUp | 按键释放 |
onKeyPress | 按键按下(已废弃,使用 onKeyDown) |
function KeyboardEventsDemo() { const [lastKey, setLastKey] = useState(''); const [keys, setKeys] = useState([]); const handleKeyDown = (event) => { setLastKey(event.key); setKeys(prev => [...prev.slice(-9), event.key].filter(k => k !== 'Backspace')); }; return ( <div> <input onKeyDown={handleKeyDown} placeholder="在这里按键..." style={{ padding: '10px', width: '100%', boxSizing: 'border-box' }} autoFocus /> <div style={{ marginTop: '10px' }}> <p>最后按下的键: <strong style={{ color: '#3b82f6' }}>{lastKey || '无'}</strong></p> <p>按键历史: {keys.map((k, i) => ( <span key={i} style={{ display: 'inline-block', padding: '2px 8px', margin: '2px', background: '#e0e7ff', borderRadius: '4px', fontSize: '12px' }}>{k}</span> ))}</p> </div> </div> ); }
键盘事件对象的重要属性:
key:按键的字符串值(如"Enter","a","1")code:按键的物理代码(如"KeyA","Digit1")keyCode:按键码(已废弃)altKey,ctrlKey,metaKey,shiftKey:修饰键是否按下
function ShortcutDemo() { const [message, setMessage] = useState(''); const handleKeyDown = (event) => { // 检测快捷键组合 if (event.ctrlKey && event.key === 's') { event.preventDefault(); setMessage('检测到 Ctrl+S 保存快捷键'); } else if (event.ctrlKey && event.key === 'z') { event.preventDefault(); setMessage('检测到 Ctrl+Z 撤销快捷键'); } else if (event.key === 'Escape') { setMessage('检测到 ESC 键'); } }; return ( <div tabIndex={0} onKeyDown={handleKeyDown} style={{ padding: '20px', background: '#fef3c7', borderRadius: '8px', outline: 'none' }} > <p>点击此处后尝试按快捷键:</p> <ul style={{ fontSize: '14px', color: '#666' }}> <li>Ctrl + S:保存</li> <li>Ctrl + Z:撤销</li> <li>ESC:取消</li> </ul> {message && <p style={{ color: '#d97706', fontWeight: 'bold' }}>{message}</p>} </div> ); }
表单事件
表单事件用于处理用户输入。
| 事件名 | 触发时机 |
|---|---|
onChange | 值变化时 |
onInput | 输入时(原生事件) |
onSubmit | 表单提交 |
onFocus | 获得焦点 |
onBlur | 失去焦点 |
function FormEventsDemo() { const [text, setText] = useState(''); const [focusState, setFocusState] = useState('未聚焦'); return ( <div> <input value={text} onChange={(e) => setText(e.target.value)} onFocus={() => setFocusState('已聚焦')} onBlur={() => setFocusState('已失焦')} placeholder="输入内容..." style={{ padding: '10px', width: '100%', boxSizing: 'border-box' }} /> <div style={{ marginTop: '10px', fontSize: '14px' }}> <p>输入值: <strong>{text || '(空)'}</strong></p> <p>焦点状态: <span style={{ color: focusState === '已聚焦' ? '#22c55e' : '#666' }}>{focusState}</span></p> <p>字符数: {text.length}</p> </div> </div> ); }
焦点事件
焦点事件与表单元素交互密切相关。
| 事件名 | 触发时机 |
|---|---|
onFocus | 元素获得焦点 |
onBlur | 元素失去焦点 |
function FocusDemo() { const inputRef = useRef(null); const focusInput = () => { inputRef.current?.focus(); }; const blurInput = () => { inputRef.current?.blur(); }; return ( <div> <input ref={inputRef} placeholder="编程式控制焦点..." style={{ padding: '10px', width: '200px' }} /> <div style={{ marginTop: '10px' }}> <button onClick={focusInput} style={{ padding: '8px 16px' }}>聚焦</button> <button onClick={blurInput} style={{ marginLeft: '10px', padding: '8px 16px' }}>失焦</button> </div> </div> ); }
事件处理的最佳实践
1. 避免在渲染中创建函数
虽然在渲染中创建箭头函数是常见的做法,但在某些性能敏感场景下,可以使用 useCallback 来缓存函数。
// 简单场景:内联函数完全可接受
function SimpleButton({ onClick }) {
return <button onClick={() => onClick(id)}>点击</button>;
}
// 性能敏感场景:使用 useCallback
function OptimizedList({ items, onSelect }) {
const handleClick = useCallback((id) => {
onSelect(id);
}, [onSelect]);
return items.map(item => (
<button key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</button>
));
}
2. 正确处理事件对象的异步访问
由于 React 的事件池机制,在异步代码中访问事件对象可能会遇到问题。如果需要在异步代码中使用事件对象,应该先保存需要的值。
// React 17+ 已移除事件池,但保存值的做法仍是好习惯
function AsyncDemo() {
const handleClick = (event) => {
// 保存需要的值
const value = event.target.value;
// 异步操作
setTimeout(() => {
console.log(value); // 正确
// console.log(event.target.value); // 可能在旧版本中出错
}, 1000);
};
}
3. 使用事件委托提高性能
当列表中有大量需要绑定事件的元素时,可以在父元素上使用事件委托。
function EventDelegationDemo() { const items = Array.from({ length: 20 }, (_, i) => ({ id: i, name: `项目 ${i + 1}` })); const [selectedId, setSelectedId] = useState(null); // 使用事件委托:在父元素上绑定一个事件处理器 const handleItemClick = (event) => { const target = event.target; if (target.dataset.id) { setSelectedId(Number(target.dataset.id)); } }; return ( <div onClick={handleItemClick}> <p>选中: {selectedId !== null ? `项目 ${selectedId + 1}` : '无'}</p> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}> {items.map(item => ( <span key={item.id} data-id={item.id} style={{ padding: '8px 16px', background: selectedId === item.id ? '#3b82f6' : '#e2e8f0', color: selectedId === item.id ? 'white' : 'black', borderRadius: '4px', cursor: 'pointer' }} > {item.name} </span> ))} </div> </div> ); }
事件委托的优势:
- 减少事件处理器数量,节省内存
- 动态添加的元素自动获得事件处理能力
- 代码更简洁
4. 防抖与节流
对于高频触发的事件(如 onMouseMove, onScroll, onResize),应该使用防抖或节流来优化性能。
function DebounceDemo() { const [searchTerm, setSearchTerm] = useState(''); const [debouncedTerm, setDebouncedTerm] = useState(''); const timeoutRef = useRef(null); const handleChange = (e) => { const value = e.target.value; setSearchTerm(value); // 清除之前的定时器 if (timeoutRef.current) { clearTimeout(timeoutRef.current); } // 设置新的定时器(防抖 500ms) timeoutRef.current = setTimeout(() => { setDebouncedTerm(value); }, 500); }; // 清理 useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); return ( <div> <input value={searchTerm} onChange={handleChange} placeholder="输入搜索词(防抖 500ms)..." style={{ padding: '10px', width: '100%', boxSizing: 'border-box' }} /> <div style={{ marginTop: '10px' }}> <p>实时值: <strong>{searchTerm}</strong></p> <p>防抖值: <strong style={{ color: '#22c55e' }}>{debouncedTerm}</strong></p> <p style={{ fontSize: '12px', color: '#666' }}>防抖值在停止输入 500ms 后更新</p> </div> </div> ); }
自定义事件处理 Hook
对于复杂的事件处理逻辑,可以提取为自定义 Hook。
useClickOutside:点击外部检测
function useClickOutside(ref, callback) { useEffect(() => { const handleClick = (event) => { if (ref.current && !ref.current.contains(event.target)) { callback(); } }; document.addEventListener('mousedown', handleClick); return () => document.removeEventListener('mousedown', handleClick); }, [ref, callback]); } function DropdownMenu() { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); useClickOutside(dropdownRef, () => setIsOpen(false)); return ( <div ref={dropdownRef} style={{ position: 'relative', display: 'inline-block' }}> <button onClick={() => setIsOpen(!isOpen)} style={{ padding: '8px 16px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px' }} > 切换菜单 </button> {isOpen && ( <div style={{ position: 'absolute', top: '100%', left: 0, marginTop: '4px', padding: '8px', background: 'white', border: '1px solid #e2e8f0', borderRadius: '4px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', minWidth: '150px' }}> <div style={{ padding: '8px', cursor: 'pointer' }} onClick={() => setIsOpen(false)}>选项 1</div> <div style={{ padding: '8px', cursor: 'pointer' }} onClick={() => setIsOpen(false)}>选项 2</div> <div style={{ padding: '8px', cursor: 'pointer' }} onClick={() => setIsOpen(false)}>选项 3</div> </div> )} </div> ); }
useKeyPress:键盘快捷键
function useKeyPress(targetKey, callback) {
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === targetKey) {
callback(event);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [targetKey, callback]);
}
// 使用
function Modal({ onClose }) {
useKeyPress('Escape', onClose);
return <div>按 ESC 关闭</div>;
}
TypeScript 中的事件类型
在 TypeScript 中使用 React 事件时,需要正确指定事件类型。
常用事件类型
import type {
MouseEvent,
KeyboardEvent,
ChangeEvent,
FormEvent,
FocusEvent,
ClipboardEvent,
DragEvent
} from 'react';
// 鼠标事件
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
};
// 键盘事件
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
// ...
}
};
// 表单变化事件
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
// 表单提交事件
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
// 焦点事件
const handleFocus = (e: FocusEvent<HTMLInputElement>) => {
// ...
};
泛型参数说明
事件类型接受一个泛型参数,指定事件目标的元素类型:
MouseEvent<HTMLButtonElement> // 鼠标事件,目标为 button
ChangeEvent<HTMLInputElement> // 变化事件,目标为 input
FormEvent<HTMLFormElement> // 表单事件,目标为 form
小结
- 命名约定:React 事件使用驼峰命名(
onClick),传递函数引用而非字符串 - 合成事件:React 实现了跨浏览器的合成事件系统,接口与原生事件一致
- 事件对象:包含
target、currentTarget、nativeEvent等属性 - 阻止行为:使用
preventDefault()阻止默认行为,stopPropagation()阻止冒泡 - 事件类型:鼠标、键盘、表单、焦点等事件各有不同的触发时机和属性
- 最佳实践:事件委托、防抖节流、正确处理异步访问
- 自定义 Hook:将复杂事件逻辑封装为可复用的 Hook
- TypeScript:使用正确的事件类型获得类型安全
练习
- 实现一个自定义右键菜单组件,使用
onContextMenu事件 - 使用键盘事件实现一个简单的快捷键系统(支持组合键)
- 实现一个
useClickOutsideHook,用于检测点击外部关闭弹窗 - 使用事件委托优化一个包含 100 个可点击项目的列表
- 实现一个搜索输入框,使用防抖优化 API 请求