React事件处理

发布于:2025-07-18 ⋅ 阅读:(10) ⋅ 点赞:(0)

下面,我们来系统的梳理关于 事件处理 的基本知识点:


一、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 最佳实践

  1. 命名规范:事件处理函数以 handle 开头(handleClick)
  2. 组件解耦:事件处理器与展示逻辑分离
  3. 性能优化:复杂操作使用节流/防抖
  4. 清理资源:移除全局事件监听器
  5. 无障碍性:支持键盘和屏幕阅读器操作

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>

总结

  1. 事件绑定:使用 camelCase 命名,传递函数而非字符串
  2. 合成事件:React 封装的事件对象,跨浏览器一致
  3. 事件类型:掌握常见事件类型及其应用场景
  4. 性能优化:避免内联函数,合理使用 useCallback 和 React.memo
  5. 高级模式:实现拖拽、手势等复杂交互
  6. 最佳实践:命名规范、清理资源、测试覆盖
  7. 常见问题:this 绑定、性能问题、动态列表处理

网站公告

今日签到

点亮在社区的每一天
去签到