下面,我们来系统的梳理关于 事件处理 的基本知识点:
一、React 事件基础
1.1 React 事件与传统 DOM 事件区别
特性 | 传统 DOM 事件 | React 事件 |
---|---|---|
命名 | 全小写 (onclick) | camelCase (onClick) |
事件处理 | 字符串 | 函数 |
默认行为 | return false 阻止 | 显式调用 e.preventDefault() |
事件对象 | 原生事件对象 | 合成事件对象 |
事件委托 | 手动实现 | 自动委托到根节点 |
1.2 基本事件绑定
function Button() {
// 事件处理函数
const handleClick = () => {
console.log('按钮被点击');
};
return (
<button onClick={handleClick}>
点击我
</button>
);
}
1.3 事件绑定方式
// 方式1: 类组件方法绑定
class Button extends React.Component {
handleClick() {
console.log('类组件点击');
}
render() {
return <button onClick={this.handleClick.bind(this)}>按钮</button>;
}
}
// 方式2: 箭头函数绑定
class Button extends React.Component {
handleClick = () => {
console.log('箭头函数绑定');
};
render() {
return <button onClick={this.handleClick}>按钮</button>;
}
}
// 方式3: 内联箭头函数
function Button() {
return (
<button onClick={() => console.log('内联函数')}>
按钮
</button>
);
}
// 方式4: useCallback 优化
function Button() {
const handleClick = useCallback(() => {
console.log('优化后的函数');
}, []);
return <button onClick={handleClick}>按钮</button>;
}
二、事件对象详解
2.1 合成事件 (SyntheticEvent)
React 封装了原生事件对象,提供跨浏览器一致性:
function InputField() {
const handleChange = (e) => {
// 获取输入值
console.log(e.target.value);
// 阻止默认行为
e.preventDefault();
// 阻止事件冒泡
e.stopPropagation();
// 合成事件属性
console.log(e.nativeEvent); // 原生事件对象
console.log(e.currentTarget); // 事件绑定元素
console.log(e.timeStamp); // 事件发生时间
};
return <input type="text" onChange={handleChange} />;
}
2.2 事件池机制
React 17 之前使用事件池机制提升性能:
function handleClick(e) {
// React 17 前:异步访问事件属性会出错
setTimeout(() => {
console.log(e.target); // null
}, 0);
// 解决方案:持久化事件对象
e.persist();
setTimeout(() => {
console.log(e.target); // 正常访问
}, 0);
}
React 17+ 移除了事件池机制,无需再调用
e.persist()
三、事件类型与处理
3.1 常见事件类型
事件类别 | 事件名称 | 说明 |
---|---|---|
鼠标事件 | onClick, onMouseDown, onMouseUp | 点击相关事件 |
onMouseEnter, onMouseLeave | 鼠标进入/离开 | |
onMouseMove | 鼠标移动 | |
表单事件 | onChange, onInput | 输入变化 |
onSubmit | 表单提交 | |
onFocus, onBlur | 焦点变化 | |
键盘事件 | onKeyDown, onKeyUp | 按键按下/释放 |
onKeyPress | 按键按下 (已弃用) | |
触摸事件 | onTouchStart, onTouchEnd | 触摸开始/结束 |
onTouchMove | 触摸移动 | |
滚动事件 | onScroll | 滚动事件 |
拖拽事件 | onDragStart, onDrop | 拖拽开始/结束 |
3.2 表单事件处理
function SignupForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});
// 通用输入处理
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
// 表单提交
const handleSubmit = (e) => {
e.preventDefault();
console.log('表单提交:', formData);
};
return (
<form onSubmit={handleSubmit}>
<input
name="username"
value={formData.username}
onChange={handleInputChange}
placeholder="用户名"
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
placeholder="邮箱"
/>
<input
name="password"
type="password"
value={formData.password}
onChange={handleInputChange}
placeholder="密码"
/>
<button type="submit">注册</button>
</form>
);
}
3.3 键盘事件处理
function SearchBar() {
const [query, setQuery] = useState('');
const handleKeyDown = (e) => {
// 回车键搜索
if (e.key === 'Enter') {
performSearch();
}
// ESC键清空
if (e.key === 'Escape') {
setQuery('');
}
};
const performSearch = () => {
console.log('搜索:', query);
};
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="输入搜索内容..."
/>
<button onClick={performSearch}>搜索</button>
</div>
);
}
四、高级事件处理模式
4.1 自定义事件(发布-订阅模式)
// 创建事件总线
const EventBus = {
events: {},
subscribe(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
},
unsubscribe(event, callback) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(cb => cb !== callback);
},
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(callback => callback(data));
}
};
// 组件A - 发布事件
function ComponentA() {
const handleClick = () => {
EventBus.emit('customEvent', { message: 'Hello from A' });
};
return <button onClick={handleClick}>发送事件</button>;
}
// 组件B - 订阅事件
function ComponentB() {
const [message, setMessage] = useState('');
useEffect(() => {
const handleCustomEvent = (data) => {
setMessage(data.message);
};
EventBus.subscribe('customEvent', handleCustomEvent);
return () => {
EventBus.unsubscribe('customEvent', handleCustomEvent);
};
}, []);
return <div>收到消息: {message}</div>;
}
4.2 事件委托优化
function List() {
const items = ['Apple', 'Banana', 'Orange'];
// 事件委托到父元素
const handleClick = (e) => {
if (e.target.tagName === 'LI') {
console.log('点击了:', e.target.textContent);
}
};
return (
<ul onClick={handleClick}>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
五、性能优化策略
5.1 避免内联函数创建
// 不推荐:每次渲染创建新函数
<button onClick={() => handleClick(id)}>按钮</button>
// 推荐:提前绑定
<button onClick={handleClick.bind(this, id)}>按钮</button>
// 最佳:使用 data 属性
<button data-id={id} onClick={handleClick}>按钮</button>
function handleClick(e) {
const id = e.target.dataset.id;
// ...
}
5.2 使用 useCallback 优化
function Parent() {
const [count, setCount] = useState(0);
// 普通函数 - 每次渲染重新创建
const handleClick = () => {
console.log('点击');
};
// useCallback 优化 - 依赖不变时函数不变
const memoizedHandleClick = useCallback(() => {
console.log('优化的点击');
}, []);
return (
<div>
<Child onClick={memoizedHandleClick} />
<button onClick={() => setCount(c => c + 1)}>
重渲染 ({count})
</button>
</div>
);
}
const Child = React.memo(function Child({ onClick }) {
console.log('子组件渲染');
return <button onClick={onClick}>子按钮</button>;
});
5.3 节流与防抖
// 使用 lodash 实现
import { throttle, debounce } from 'lodash';
function ScrollComponent() {
// 节流:每200ms最多执行一次
const handleScrollThrottled = useCallback(throttle(() => {
console.log('滚动位置:', window.scrollY);
}, 200), []);
// 防抖:停止滚动200ms后执行
const handleScrollDebounced = useCallback(debounce(() => {
console.log('滚动结束');
}, 200), []);
useEffect(() => {
window.addEventListener('scroll', handleScrollThrottled);
window.addEventListener('scroll', handleScrollDebounced);
return () => {
window.removeEventListener('scroll', handleScrollThrottled);
window.removeEventListener('scroll', handleScrollDebounced);
handleScrollThrottled.cancel();
handleScrollDebounced.cancel();
};
}, [handleScrollThrottled, handleScrollDebounced]);
return <div style={{ height: '200vh' }}>滚动测试</div>;
}
// 自定义实现
function useDebounce(callback, delay) {
const timerRef = useRef();
return useCallback((...args) => {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
}
六、复杂交互模式
6.1 拖拽事件实现
function DraggableBox() {
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 = useCallback((e) => {
if (!dragging) return;
setPosition({
x: e.clientX - offset.x,
y: e.clientY - offset.y
});
}, [dragging, offset]);
const handleMouseUp = () => {
setDragging(false);
};
useEffect(() => {
if (dragging) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
}
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}, [dragging, handleMouseMove]);
return (
<div
style={{
position: 'absolute',
left: position.x,
top: position.y,
cursor: dragging ? 'grabbing' : 'grab',
width: 100,
height: 100,
backgroundColor: 'lightblue',
border: '1px solid blue'
}}
onMouseDown={handleMouseDown}
>
拖拽我
</div>
);
}
6.2 手势事件处理
function PinchZoom() {
const [scale, setScale] = useState(1);
const touchRef = useRef({});
const handleTouchStart = (e) => {
if (e.touches.length === 2) {
const [t1, t2] = e.touches;
touchRef.current = {
distance: Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY),
scale
};
}
};
const handleTouchMove = (e) => {
if (e.touches.length === 2) {
const [t1, t2] = e.touches;
const currentDistance = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
if (touchRef.current.distance) {
const newScale = touchRef.current.scale *
(currentDistance / touchRef.current.distance);
setScale(Math.min(Math.max(newScale, 0.5), 3));
}
}
};
return (
<div
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
style={{
transform: `scale(${scale})`,
transformOrigin: 'center',
width: '100%',
height: '300px',
backgroundColor: '#f0f0f0',
touchAction: 'none'
}}
>
双指缩放
</div>
);
}
七、测试与调试
7.1 事件测试
import { render, screen, fireEvent } from '@testing-library/react';
test('按钮点击触发事件', () => {
const handleClick = jest.fn();
render(<button onClick={handleClick}>点击</button>);
fireEvent.click(screen.getByText('点击'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
test('输入变化更新状态', () => {
render(<InputField />);
const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: '新值' } });
expect(input.value).toBe('新值');
});
test('表单提交阻止默认行为', () => {
const handleSubmit = jest.fn(e => e.preventDefault());
render(<form onSubmit={handleSubmit} />);
const form = screen.getByRole('form');
fireEvent.submit(form);
expect(handleSubmit).toHaveBeenCalled();
});
7.2 事件调试工具
- React DevTools:查看组件事件处理器
- Chrome 事件监听器:检查元素绑定事件
- SyntheticEvent 日志:
console.log(e.nativeEvent)
- 性能分析:React Profiler 分析事件处理性能
八、最佳实践与常见问题
8.1 最佳实践
- 命名规范:事件处理函数以
handle
开头(handleClick) - 组件解耦:事件处理器与展示逻辑分离
- 性能优化:复杂操作使用节流/防抖
- 清理资源:移除全局事件监听器
- 无障碍性:支持键盘和屏幕阅读器操作
8.2 常见问题解决方案
问题1:事件处理函数中 this 未定义
class MyComponent extends React.Component {
constructor() {
super();
// 解决方案1: 构造函数绑定
this.handleClick = this.handleClick.bind(this);
}
// 解决方案2: 箭头函数
handleClick = () => {
console.log(this); // 正确指向组件实例
};
render() {
return <button onClick={this.handleClick}>按钮</button>;
}
}
问题2:事件频繁触发导致性能问题
function SearchBox() {
const [query, setQuery] = useState('');
// 防抖处理搜索请求
const debouncedSearch = useDebounce(() => {
searchApi(query);
}, 300);
useEffect(() => {
debouncedSearch();
}, [query, debouncedSearch]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
问题3:事件委托处理动态列表
function DynamicList() {
const [items, setItems] = useState(['A', 'B', 'C']);
// 事件委托到父元素
const handleClick = (e) => {
if (e.target.dataset.index) {
const index = parseInt(e.target.dataset.index);
console.log('点击了:', items[index]);
}
};
return (
<ul onClick={handleClick}>
{items.map((item, index) => (
<li key={index} data-index={index}>{item}</li>
))}
</ul>
);
}
九、实战案例:多功能按钮组件
function ActionButton({
onClick,
onDoubleClick,
onMouseDown,
onMouseUp,
onContextMenu,
children
}) {
// 点击事件
const handleClick = (e) => {
if (onClick) onClick(e);
};
// 双击事件
let clickTimeout;
const handleDoubleClick = (e) => {
clearTimeout(clickTimeout);
if (onDoubleClick) onDoubleClick(e);
};
// 单击/双击处理
const handleSingleOrDouble = (e) => {
clearTimeout(clickTimeout);
clickTimeout = setTimeout(() => {
if (onClick) onClick(e);
}, 250);
};
return (
<button
onClick={handleSingleOrDouble}
onDoubleClick={handleDoubleClick}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onContextMenu={(e) => {
e.preventDefault();
if (onContextMenu) onContextMenu(e);
}}
style={{ padding: '10px 20px', cursor: 'pointer' }}
>
{children}
</button>
);
}
// 使用示例
<ActionButton
onClick={() => console.log('单击')}
onDoubleClick={() => console.log('双击')}
onContextMenu={() => console.log('右键菜单')}
>
多功能按钮
</ActionButton>
总结
- 事件绑定:使用 camelCase 命名,传递函数而非字符串
- 合成事件:React 封装的事件对象,跨浏览器一致
- 事件类型:掌握常见事件类型及其应用场景
- 性能优化:避免内联函数,合理使用 useCallback 和 React.memo
- 高级模式:实现拖拽、手势等复杂交互
- 最佳实践:命名规范、清理资源、测试覆盖
- 常见问题:this 绑定、性能问题、动态列表处理