跳到主要内容

事件处理

事件处理是 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>
  );
}
结果
Loading...

内联箭头函数

对于简单的逻辑,可以直接使用内联箭头函数。

实时编辑器
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>
  );
}
结果
Loading...

传递参数给事件处理器

当需要传递额外参数时,可以使用箭头函数包装。

实时编辑器
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>
  );
}
结果
Loading...

另一种方式是使用 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>
  );
}
结果
Loading...

合成事件的主要属性

属性说明
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>
  );
}
结果
Loading...

当点击 <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>
  );
}
结果
Loading...

常见需要阻止默认行为的场景:

  • 表单提交(阻止页面刷新)
  • 链接跳转(阻止页面导航)
  • 右键菜单(阻止显示默认菜单)
  • 文本选择(阻止选中文字)

阻止事件冒泡

使用 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>
  );
}
结果
Loading...

事件传播的三个阶段

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>
  );
}
结果
Loading...

键盘事件

键盘事件用于处理用户的键盘输入。

事件名触发时机
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>
  );
}
结果
Loading...

键盘事件对象的重要属性:

  • 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>
  );
}
结果
Loading...

表单事件

表单事件用于处理用户输入。

事件名触发时机
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>
  );
}
结果
Loading...

焦点事件

焦点事件与表单元素交互密切相关。

事件名触发时机
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>
  );
}
结果
Loading...

事件处理的最佳实践

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>
  );
}
结果
Loading...

事件委托的优势:

  • 减少事件处理器数量,节省内存
  • 动态添加的元素自动获得事件处理能力
  • 代码更简洁

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>
  );
}
结果
Loading...

自定义事件处理 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>
  );
}
结果
Loading...

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

小结

  1. 命名约定:React 事件使用驼峰命名(onClick),传递函数引用而非字符串
  2. 合成事件:React 实现了跨浏览器的合成事件系统,接口与原生事件一致
  3. 事件对象:包含 targetcurrentTargetnativeEvent 等属性
  4. 阻止行为:使用 preventDefault() 阻止默认行为,stopPropagation() 阻止冒泡
  5. 事件类型:鼠标、键盘、表单、焦点等事件各有不同的触发时机和属性
  6. 最佳实践:事件委托、防抖节流、正确处理异步访问
  7. 自定义 Hook:将复杂事件逻辑封装为可复用的 Hook
  8. TypeScript:使用正确的事件类型获得类型安全

练习

  1. 实现一个自定义右键菜单组件,使用 onContextMenu 事件
  2. 使用键盘事件实现一个简单的快捷键系统(支持组合键)
  3. 实现一个 useClickOutside Hook,用于检测点击外部关闭弹窗
  4. 使用事件委托优化一个包含 100 个可点击项目的列表
  5. 实现一个搜索输入框,使用防抖优化 API 请求

参考资源