React: 事件处理
最后更新:2026-08-26
事件处理是 React 应用的 神经末梢——用户点击按钮、输入文字、提交表单,这些交互动作通过事件来响应。React 的事件系统比原生 DOM 事件更智能、更统一。
1. 你将学到
- React 合成事件与原生事件的区别
- 常用事件:onClick / onChange / onSubmit / onKeyDown
- 事件对象的处理
- 向事件处理函数传递参数
- 阻止默认行为和事件冒泡
2. 一个表单交互的故事
(1) 痛点:表单提交的 3 个坑
Bob 在做一个用户注册表单,用原生 JavaScript 写事件处理:
JAVASCRIPT
// 原生 JS:需要手动处理各种兼容性和细节
// 1. 获取 DOM 元素
const form = document.getElementById('register-form')
const nameInput = document.getElementById('name')
const submitBtn = document.getElementById('submit-btn')
// 2. 绑定事件
form.addEventListener('submit', function(event) {
event.preventDefault() // 阻止页面刷新
// 3. 手动获取表单数据
const formData = new FormData(form)
const data = Object.fromEntries(formData)
// 4. 提交数据
fetch('/api/register', {
method: 'POST',
body: JSON.stringify(data)
})
})
// 5. 输入实时校验
nameInput.addEventListener('input', function(event) {
if (event.target.value.length < 2) {
showError('姓名至少 2 个字符')
} else {
hideError()
}
})
// 6. 点击提交按钮时防止重复提交
let isSubmitting = false
submitBtn.addEventListener('click', function() {
if (isSubmitting) return
isSubmitting = true
submitBtn.disabled = true
submitBtn.textContent = '提交中...'
})
Bob 遇到的问题:
- 代码分散:事件绑定、数据处理、UI 更新分散在不同地方
- 内存泄漏:忘记移除事件监听器
- 兼容性问题:不同浏览器事件 API 有差异
(2) React 的事件处理
JSX
function RegisterForm() {
const [name, setName] = React.useState('')
const [isSubmitting, setIsSubmitting] = React.useState(false)
// 提交处理
function handleSubmit(event) {
event.preventDefault()
setIsSubmitting(true)
fetch('/api/register', {
method: 'POST',
body: JSON.stringify({ name })
}).finally(() => setIsSubmitting(false))
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="请输入姓名"
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '提交中...' : '注册'}
</button>
</form>
)
}
收益:代码从 30 行缩到 15 行,没有 DOM 操作,没有事件解绑问题,没有浏览器兼容问题。
3. React 合成事件
React 的事件不是原生 DOM 事件,而是合成事件(SyntheticEvent)——React 在顶层统一监听,模拟了一套跨浏览器的事件系统。
| 特性 | 原生 DOM 事件 | React 合成事件 |
|---|---|---|
| 绑定方式 | addEventListener / onclick |
直接在 JSX 上写 |
| 浏览器兼容 | 需手动处理差异 | 自动统一 |
| 内存管理 | 需手动移除监听器 | 自动清理 |
| 事件对象 | 原生 Event | 合成 SyntheticEvent |
| 阻止冒泡 | event.stopPropagation() |
相同(标准化) |
| 阻止默认 | event.preventDefault() |
相同(标准化) |
graph TB
A[用户点击按钮] --> B[React 根节点捕获事件]
B --> C[创建合成事件对象]
C --> D[模拟冒泡/捕获阶段]
D --> E[调用 JSX 中绑定的 handler]
style B fill:#61dafb,color:#000
style C fill:#1890ff,color:#fff
4. 常用事件速查
| 事件名 | 触发时机 | 常用场景 | 事件对象类型 |
|---|---|---|---|
onClick |
元素被点击 | 按钮、链接、卡片 | MouseEvent |
onChange |
输入框内容变化 | 表单输入、选择框 | ChangeEvent |
onSubmit |
表单提交 | 登录/注册/搜索 | FormEvent |
onFocus |
元素获得焦点 | 输入框高亮 | FocusEvent |
onBlur |
元素失去焦点 | 输入校验 | FocusEvent |
onKeyDown |
按键按下 | 快捷键、回车搜索 | KeyboardEvent |
onKeyUp |
按键抬起 | 实时搜索 | KeyboardEvent |
onMouseEnter |
鼠标进入 | 悬停效果 | MouseEvent |
onMouseLeave |
鼠标离开 | 悬停效果 | MouseEvent |
onScroll |
滚动 | 无限滚动加载 | UIEvent |
▶ 示例:常用事件综合展示
JSX
📖 仅展示
// ============================================
// 示例:一个登录表单的完整事件处理
// ============================================
function LoginForm() {
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [errors, setErrors] = React.useState({})
const [focusedField, setFocusedField] = React.useState('')
function handleSubmit(event) {
event.preventDefault()
// 表单校验
const newErrors = {}
if (!email.includes('@')) newErrors.email = '邮箱格式不正确'
if (password.length < 6) newErrors.password = '密码至少 6 位'
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
// 提交登录
console.log('登录:', { email, password })
}
function handleKeyDown(event) {
// 按 Escape 键清除焦点
if (event.key === 'Escape') {
event.target.blur()
}
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: '400px', margin: '0 auto' }}>
<h2>登录</h2>
{/* 邮箱输入 */}
<div style={{ marginBottom: '16px' }}>
<label>邮箱:</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
onFocus={() => setFocusedField('email')}
onBlur={() => { setFocusedField(''); setErrors({...errors, email: undefined}) }}
onKeyDown={handleKeyDown}
style={{
width: '100%',
padding: '8px',
borderColor: errors.email ? '#ff4d4f' : focusedField === 'email' ? '#1890ff' : '#d9d9d9'
}}
/>
{errors.email && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.email}</p>}
</div>
{/* 密码输入 */}
<div style={{ marginBottom: '16px' }}>
<label>密码:</label>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
onFocus={() => setFocusedField('password')}
onBlur={() => setFocusedField('')}
onKeyDown={handleKeyDown}
style={{
width: '100%',
padding: '8px',
borderColor: errors.password ? '#ff4d4f' : focusedField === 'password' ? '#1890ff' : '#d9d9d9'
}}
/>
{errors.password && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.password}</p>}
</div>
{/* 提交按钮 */}
<button
type="submit"
onClick={() => console.log('点击了登录按钮')}
onMouseEnter={() => console.log('鼠标进入按钮')}
onMouseLeave={() => console.log('鼠标离开按钮')}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#1890ff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
登录
</button>
</form>
)
}
5. 事件参数传递
(1) 直接传参
用箭头函数包裹,直接传入额外参数:
JSX
function UserList({ users, onDelete }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
{/* 箭头函数:传入 user.id 作为额外参数 */}
<button onClick={() => onDelete(user.id)}>
删除
</button>
</li>
))}
</ul>
)
}
// 使用
<UserList
users={users}
onDelete={(id) => console.log('删除用户:', id)}
/>
(2) 同时获取事件对象和自定义参数
JSX
function ColorPicker({ colors, onSelect }) {
return (
<div>
<p>选择颜色:</p>
{colors.map(color => (
<button
key={color}
onClick={(event) => {
// event:原生合成事件对象
// color:自定义参数
onSelect(color)
console.log('点击位置:', event.clientX, event.clientY)
}}
style={{
backgroundColor: color,
width: '40px',
height: '40px',
border: '2px solid #ddd',
borderRadius: '50%',
margin: '4px',
cursor: 'pointer'
}}
/>
))}
</div>
)
}
(3) 传递事件对象的 3 种方式
| 方式 | 写法 | 适用场景 |
|---|---|---|
| 隐式传递 | onClick={handleClick} |
不需要额外参数 |
| 箭头函数 | onClick={() => handleClick(id)} |
需要传递参数 |
| 同时传递 | onClick={(e) => handleClick(e, id)} |
需要事件对象 + 自定义参数 |
6. 阻止默认行为和冒泡
(1) 阻止默认行为:preventDefault()
最常用的场景是阻止表单提交刷新页面:
JSX
function SearchForm() {
const [query, setQuery] = React.useState('')
function handleSubmit(event) {
event.preventDefault() // 阻止页面刷新
// 执行自定义搜索逻辑
console.log('搜索:', query)
}
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="搜索..."
/>
<button type="submit">搜索</button>
</form>
)
}
// 其他需要阻止默认行为的场景:
// - 链接的跳转:<a href="#" onClick={e => e.preventDefault()}>
// - 右键菜单:onContextMenu={e => e.preventDefault()}
// - 拖拽文件上传:onDragOver={e => e.preventDefault()}
(2) 阻止冒泡:stopPropagation()
JSX
function Modal() {
return (
// 点击遮罩层关闭弹窗
<div
style={overlayStyle}
onClick={() => console.log('点击遮罩层 → 关闭弹窗')}
>
{/* 点击弹窗内容不冒泡 */}
<div
style={modalStyle}
onClick={(event) => {
event.stopPropagation() // 阻止冒泡到遮罩层
console.log('点击弹窗内容')
}}
>
<h2>弹窗标题</h2>
<p>弹窗内容</p>
<button onClick={() => console.log('关闭')}>关闭</button>
</div>
</div>
)
}
// 点击"弹窗内容"区 → 只触发弹窗的 onClick
// 点击"遮罩层"(弹窗外)→ 触发遮罩层的 onClick
7. 完整示例:Todo 列表(综合事件处理)
JSX
// ============================================
// 示例:Todo 列表(综合事件处理)
// 功能:添加任务、标记完成、删除任务、双击编辑
// ============================================
function TodoApp() {
const [todos, setTodos] = React.useState([
{ id: 1, text: '学习 React 事件', done: false },
{ id: 2, text: '完成作业', done: false }
])
const [input, setInput] = React.useState('')
// 1. 添加任务(onSubmit + 回车键)
function handleSubmit(event) {
event.preventDefault()
if (!input.trim()) return
setTodos([...todos, {
id: Date.now(),
text: input.trim(),
done: false
}])
setInput('')
}
// 2. 回车键快捷添加(onKeyDown)
function handleKeyDown(event) {
if (event.key === 'Enter' && input.trim()) {
handleSubmit(event)
}
}
// 3. 切换完成状态(onChange)
function handleToggle(id) {
setTodos(todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
))
}
// 4. 删除任务(onClick + 参数传递)
function handleDelete(id, event) {
event.stopPropagation() // 防止触发 li 的事件
setTodos(todos.filter(t => t.id !== id))
}
// 5. 双击编辑(onDoubleClick)
function handleEdit(todo) {
const newText = prompt('编辑任务:', todo.text)
if (newText && newText.trim()) {
setTodos(todos.map(t =>
t.id === todo.id ? { ...t, text: newText.trim() } : t
))
}
}
return (
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>📋 Todo 列表</h2>
{/* 输入表单 */}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="输入任务,按回车添加..."
style={{
width: '70%', padding: '8px',
border: '1px solid #d9d9d9', borderRadius: '4px'
}}
/>
<button type="submit" style={{
padding: '8px 16px', marginLeft: '8px',
backgroundColor: '#1890ff', color: 'white',
border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
添加
</button>
</form>
{/* 统计 */}
<p style={{ color: '#666', fontSize: '14px' }}>
总 {todos.length} 项,已完成 {todos.filter(t => t.done).length} 项
</p>
{/* 任务列表 */}
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map(todo => (
<li
key={todo.id}
onDoubleClick={() => handleEdit(todo)}
style={{
display: 'flex', alignItems: 'center',
padding: '10px', margin: '4px 0',
backgroundColor: todo.done ? '#f6ffed' : '#fff',
border: '1px solid #f0f0f0',
borderRadius: '4px',
textDecoration: todo.done ? 'line-through' : 'none',
color: todo.done ? '#999' : '#333',
cursor: 'pointer'
}}
>
{/* 复选框 */}
<input
type="checkbox"
checked={todo.done}
onChange={() => handleToggle(todo.id)}
style={{ marginRight: '10px' }}
/>
{/* 任务文本 */}
<span style={{ flex: 1 }}>{todo.text}</span>
{/* 删除按钮 */}
<button
onClick={(e) => handleDelete(todo.id, e)}
style={{
padding: '2px 8px',
backgroundColor: 'transparent',
color: '#ff4d4f',
border: 'none',
cursor: 'pointer',
fontSize: '16px'
}}
>
✕
</button>
</li>
))}
</ul>
</div>
)
}
交互流程:
- 输入框键入任务名,按回车或点"添加"按钮 → 任务加入列表
- 勾选复选框 → 任务标记完成(划掉文字 + 变灰)
- 点击 ✕ 按钮 → 删除该任务(
stopPropagation防止触发双击编辑)- 双击任意任务 → 弹出编辑对话框
▶ 示例 2:键盘快捷键与焦点管理
JSX
📖 仅展示
// ============================================
// 示例:快捷键面板——键盘事件与焦点管理综合运用
// 功能:使用 onKeyDown 实现快捷键操作,使用 onFocus/onBlur 管理焦点
// ============================================
function ShortcutPanel() {
const [output, setOutput] = React.useState('')
const [activeKeys, setActiveKeys] = React.useState(new Set())
const inputRef = React.useRef(null)
// 键盘事件处理
function handleKeyDown(event) {
const { key, ctrlKey, shiftKey, altKey } = event
// Ctrl+S:保存
if (ctrlKey && key === 's') {
event.preventDefault()
setOutput('💾 已保存(Ctrl+S)')
}
// Ctrl+Z:撤销
else if (ctrlKey && key === 'z') {
event.preventDefault()
setOutput('↩️ 撤销操作(Ctrl+Z)')
}
// Escape:清除输出
else if (key === 'Escape') {
setOutput('')
event.target.blur()
}
// Enter:确认
else if (key === 'Enter') {
setOutput(`✅ 确认:${event.target.value || '(空输入)'}`)
}
// 显示当前按下的键
setActiveKeys(prev => new Set([...prev, key]))
}
function handleKeyUp(event) {
setActiveKeys(prev => {
const next = new Set(prev)
next.delete(event.key)
return next
})
}
// 自动聚焦
React.useEffect(() => {
inputRef.current.focus()
}, [])
return (
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>⌨️ 快捷键面板</h2>
<input
ref={inputRef}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onFocus={() => setOutput('输入框已聚焦')}
onBlur={() => setOutput('输入框已失焦')}
placeholder="在此输入,尝试快捷键..."
style={{
width: '100%',
padding: '10px',
fontSize: '16px',
border: '2px solid #1890ff',
borderRadius: '6px',
outline: 'none'
}}
/>
{/* 快捷键提示 */}
<div style={{ marginTop: '12px', display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{[
{ key: 'Ctrl+S', desc: '保存' },
{ key: 'Ctrl+Z', desc: '撤销' },
{ key: 'Enter', desc: '确认' },
{ key: 'Escape', desc: '清除/失焦' }
].map(shortcut => (
<span key={shortcut.key} style={{
padding: '4px 10px',
backgroundColor: '#f5f5f5',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '12px',
fontFamily: 'monospace'
}}>
{shortcut.key} <span style={{ color: '#999' }}>{shortcut.desc}</span>
</span>
))}
</div>
{/* 当前按下的键 */}
{activeKeys.size > 0 && (
<p style={{ marginTop: '8px', fontSize: '13px', color: '#666' }}>
当前按下的键:{[...activeKeys].join(' + ')}
</p>
)}
{/* 操作输出 */}
{output && (
<div style={{
marginTop: '12px',
padding: '10px',
backgroundColor: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: '4px',
color: '#52c41a',
fontWeight: 'bold'
}}>
{output}
</div>
)}
</div>
)
}
▶ 示例 3:拖拽排序事件处理
JSX
function DragSortList() {
const [items, setItems] = useState(['Apple', 'Banana', 'Cherry', 'Date'])
const [dragIdx, setDragIdx] = useState(null)
function handleDragStart(idx) { setDragIdx(idx) }
function handleDragOver(e, idx) {
e.preventDefault()
if (dragIdx === null || dragIdx === idx) return
setItems(prev => {
const updated = [...prev]
const [moved] = updated.splice(dragIdx, 1)
updated.splice(idx, 0, moved)
return updated
})
setDragIdx(idx)
}
function handleDragEnd() { setDragIdx(null) }
return (
<div style={{ maxWidth: 300, margin: '0 auto' }}>
<h3>Drag to Reorder</h3>
{items.map((item, idx) => (
<div key={item} draggable
onDragStart={() => handleDragStart(idx)}
onDragOver={e => handleDragOver(e, idx)}
onDragEnd={handleDragEnd}
style={{
padding: '8px 12px', marginBottom: 4, borderRadius: 4, cursor: 'grab',
background: dragIdx === idx ? '#e6f7ff' : '#f5f5f5',
border: dragIdx === idx ? '2px solid #1890ff' : '2px solid transparent',
}}>
{item}
</div>
))}
</div>
)
}
▶ 示例 4:自定义 Hook 封装事件监听
JSX
function useEventListener(event, handler, element = window) {
useEffect(() => {
element.addEventListener(event, handler)
return () => element.removeEventListener(event, handler)
}, [event, handler, element])
}
function MouseTracker() {
const [pos, setPos] = useState({ x: 0, y: 0 })
const handler = useCallback(e => setPos({ x: e.clientX, y: e.clientY }), [])
useEventListener('mousemove', handler)
return (
<div style={{ padding: 20 }}>
<p>Mouse: ({pos.x}, {pos.y})</p>
<div style={{
width: 200, height: 200, border: '1px solid #ddd', position: 'relative', borderRadius: 4,
}}>
<div style={{
width: 10, height: 10, borderRadius: '50%', background: '#1890ff',
position: 'absolute', left: Math.min(pos.x - 100, 190), top: Math.min(pos.y - 100, 190),
transition: 'left 0.1s, top 0.1s',
}} />
</div>
</div>
)
}
▶ 示例 5:表单提交与事件组合
JSX
function SearchForm({ onSearch }) {
const [query, setQuery] = useState('')
const [category, setCategory] = useState('all')
function handleSubmit(e) {
e.preventDefault()
onSearch({ query, category })
}
function handleReset() {
setQuery('')
setCategory('all')
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<input value={query} onChange={e => setQuery(e.target.value)}
placeholder="Search..." style={{ flex: 1, padding: 8, borderRadius: 4 }} />
<select value={category} onChange={e => setCategory(e.target.value)}
style={{ padding: 8, borderRadius: 4 }}>
<option value="all">All</option>
<option value="electronics">Electronics</option>
<option value="books">Books</option>
</select>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button type="submit" style={{ padding: '6px 16px', cursor: 'pointer' }}>Search</button>
<button type="button" onClick={handleReset} style={{ padding: '6px 16px', cursor: 'pointer' }}>Reset</button>
</div>
</form>
)
}
❓ 常见问题
Q 为什么用箭头函数
() => handleClick(id) 而不是直接 handleClick(id)?A 如果直接写
onClick={handleClick(id)},React 会在渲染时立即执行 handleClick(id),而不是等到点击时才执行。因为这是函数调用(有括号),不是函数引用(没有括号)。正确做法是 onClick={() => handleClick(id)}(箭头函数延迟执行)或 onClick={handleClick}(不传参时直接引用函数名)。Q 可以在一个元素上绑定多个相同事件吗?
A 不能直接绑两个
onClick。解决方案:① 在同一个处理函数里调用多个函数 onClick={() => { fn1(); fn2() }};② 或者把元素用多个 HOC 包装。通常一个元素只需要一个事件处理函数,在里面调用多个逻辑即可。Q React 合成事件和原生事件有什么区别?
A React 的 SyntheticEvent 是对原生事件的跨浏览器封装,提供统一的 API(如 e.preventDefault()、e.stopPropagation()),在所有浏览器中行为一致。合成事件在事件委托机制下工作——所有事件都挂载到 root 节点上,而不是各个 DOM 元素。React 17+ 事件委托到 root 而非 document,避免了多 React 版本共存时的冲突。
📖 小节
- React 使用合成事件(SyntheticEvent),跨浏览器兼容,自动清理
- 常用事件:onClick、onChange、onSubmit、onKeyDown、onFocus/onBlur
- 传递额外参数用箭头函数
onClick={() => handler(id)} event.preventDefault()阻止默认行为,event.stopPropagation()阻止冒泡- 不要写
onClick={handler(id)}(会立即执行),要用onClick={() => handler(id)}
📝 作业
- 基础题(难度⭐):创建一个
ButtonCounter组件,每点击一次按钮,显示的计数值 +1。使用onClick事件。 - 进阶题(难度⭐⭐):创建一个
SearchInput组件,实现"防抖搜索":用户停止输入 500ms 后自动触发搜索。使用onChange和useEffect。 - 挑战题(难度⭐⭐⭐):创建一个
DragAndDropList组件,实现拖拽排序功能。使用onDragStart、onDragOver、onDrop事件。列表项可拖动到新位置。