React: 事件处理

最后更新:2026-08-26

事件处理是 React 应用的 神经末梢——用户点击按钮、输入文字、提交表单,这些交互动作通过事件来响应。React 的事件系统比原生 DOM 事件更智能、更统一。


1. 你将学到



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 遇到的问题:

(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() 相同(标准化)
100%
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>
  )
}
逻辑代码 81 行(超过 40 行限制,仅展示)

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>
  )
}

交互流程

  1. 输入框键入任务名,按回车或点"添加"按钮 → 任务加入列表
  2. 勾选复选框 → 任务标记完成(划掉文字 + 变灰)
  3. 点击 ✕ 按钮 → 删除该任务(stopPropagation 防止触发双击编辑)
  4. 双击任意任务 → 弹出编辑对话框

▶ 示例 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>
  )
}
逻辑代码 95 行(超过 40 行限制,仅展示)

▶ 示例 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 版本共存时的冲突。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个 ButtonCounter 组件,每点击一次按钮,显示的计数值 +1。使用 onClick 事件。
  2. 进阶题(难度⭐⭐):创建一个 SearchInput 组件,实现"防抖搜索":用户停止输入 500ms 后自动触发搜索。使用 onChangeuseEffect
  3. 挑战题(难度⭐⭐⭐):创建一个 DragAndDropList 组件,实现拖拽排序功能。使用 onDragStartonDragOveronDrop 事件。列表项可拖动到新位置。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏