React 事件处理

React中的事件处理与DOM事件处理类似,但有语法上的差异。React事件使用驼峰命名法,而不是全小写。

基本事件处理

1. 绑定事件处理器

import React, { useState } from 'react';

function ClickCounter() {
  const [count, setCount] = useState(0);

  // 事件处理函数
  const handleClick = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>点击次数: {count}</p>
      {/* 注意:onClick 驼峰命名 */}
      <button onClick={handleClick}>
        点击我
      </button>
    </div>
  );
}

export default ClickCounter;

实时演示

点击次数: 0

2. 在JSX中内联定义事件处理器

function InlineEventHandler() {
  const [message, setMessage] = useState('');

  return (
    <div>
      <p>{message}</p>
      {/* 内联箭头函数 */}
      <button onClick={() => setMessage('按钮被点击了!')}>
        显示消息
      </button>

      {/* 内联普通函数 */}
      <button onClick={function() {
        console.log('按钮被点击了');
        alert('按钮点击事件触发');
      }}>
        显示警告
      </button>
    </div>
  );
}
注意: 内联箭头函数每次渲染都会创建新的函数实例,可能影响性能。对于需要传递参数的情况可以使用,否则建议将函数定义在组件外部。

事件对象

React中的事件对象是合成事件(SyntheticEvent),是跨浏览器包装器,与原生事件接口相同。

function EventObjectDemo() {
  const handleClick = (event) => {
    // 阻止默认行为
    event.preventDefault();

    // 停止事件冒泡
    event.stopPropagation();

    // 访问事件目标
    console.log('事件目标:', event.target);
    console.log('当前目标:', event.currentTarget);
    console.log('事件类型:', event.type);

    // 鼠标事件特有属性
    console.log('鼠标位置:', event.clientX, event.clientY);
  };

  const handleChange = (event) => {
    // 输入事件
    console.log('输入的值:', event.target.value);
  };

  const handleKeyDown = (event) => {
    // 键盘事件
    console.log('按下的键:', event.key);
    console.log('键码:', event.keyCode);

    // 按下回车键
    if (event.key === 'Enter') {
      console.log('用户按下了回车键');
    }
  };

  return (
    <div onClick={handleClick}>
      <a href="https://reactjs.org" onClick={handleClick}>
        点击我(不会跳转)
      </a>
      <br />
      <input
        type="text"
        onChange={handleChange}
        onKeyDown={handleKeyDown}
        placeholder="输入并按回车"
      />
    </div>
  );
}

常见事件类型

事件类型 React属性 描述 示例
点击事件 onClick 元素被点击时触发 <button onClick={handleClick}>
变化事件 onChange 表单元素值变化时触发 <input onChange={handleChange}>
提交事件 onSubmit 表单提交时触发 <form onSubmit={handleSubmit}>
鼠标事件 onMouseEnter, onMouseLeave 鼠标进入/离开元素 <div onMouseEnter={handleMouseEnter}>
键盘事件 onKeyDown, onKeyUp 键盘按键按下/释放 <input onKeyDown={handleKeyDown}>
焦点事件 onFocus, onBlur 元素获得/失去焦点 <input onFocus={handleFocus}>

传递参数给事件处理器

方法1:使用箭头函数

function ItemList() {
  const items = ['苹果', '香蕉', '橙子', '葡萄'];

  const handleClick = (item, index, event) => {
    console.log(`你点击了: ${item}, 索引: ${index}`);
    console.log('事件对象:', event);
  };

  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>
          {/* 使用箭头函数传递参数 */}
          <button onClick={(e) => handleClick(item, index, e)}>
            选择 {item}
          </button>
        </li>
      ))}
    </ul>
  );
}

方法2:使用bind方法

class ItemListClass extends React.Component {
  handleClick(item, index, event) {
    console.log(`你点击了: ${item}, 索引: ${index}`);
    console.log('事件对象:', event);
  }

  render() {
    const items = ['苹果', '香蕉', '橙子', '葡萄'];

    return (
      <ul>
        {items.map((item, index) => (
          <li key={index}>
            {/* 使用bind传递参数 */}
            <button onClick={this.handleClick.bind(this, item, index)}>
              选择 {item}
            </button>
          </li>
        ))}
      </ul>
    );
  }
}

方法3:使用data属性

function DataAttributeDemo() {
  const handleClick = (event) => {
    const itemId = event.target.dataset.id;
    const itemName = event.target.dataset.name;
    console.log(`ID: ${itemId}, 名称: ${itemName}`);
  };

  const items = [
    { id: 1, name: '苹果' },
    { id: 2, name: '香蕉' },
    { id: 3, name: '橙子' }
  ];

  return (
    <div>
      {items.map(item => (
        <button
          key={item.id}
          data-id={item.id}
          data-name={item.name}
          onClick={handleClick}
          className="event-button"
        >
          {item.name}
        </button>
      ))}
    </div>
  );
}
性能提示: 在渲染列表时,如果使用箭头函数或bind方法传递参数,每次渲染都会创建新函数,可能影响性能。可以考虑使用data属性或事件委托。

表单事件处理

function FormExample() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    rememberMe: false,
    gender: 'male',
    country: 'china'
  });

  // 处理输入变化
  const handleInputChange = (event) => {
    const { name, value, type, checked } = event.target;

    setFormData(prevState => ({
      ...prevState,
      [name]: type === 'checkbox' ? checked : value
    }));
  };

  // 处理表单提交
  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('表单数据:', formData);
    // 发送到服务器...
    alert('表单提交成功!');
  };

  // 重置表单
  const handleReset = () => {
    setFormData({
      username: '',
      email: '',
      password: '',
      rememberMe: false,
      gender: 'male',
      country: 'china'
    });
  };

  return (
    <form onSubmit={handleSubmit} className="form-demo">
      <div className="mb-3">
        <label>用户名:</label>
        <input
          type="text"
          name="username"
          value={formData.username}
          onChange={handleInputChange}
          placeholder="请输入用户名"
        />
      </div>

      <div className="mb-3">
        <label>邮箱:</label>
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleInputChange}
          placeholder="example@email.com"
        />
      </div>

      <div className="mb-3">
        <label>密码:</label>
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleInputChange}
        />
      </div>

      <div className="mb-3">
        <label>
          <input
            type="checkbox"
            name="rememberMe"
            checked={formData.rememberMe}
            onChange={handleInputChange}
          />
          记住我
        </label>
      </div>

      <div className="mb-3">
        <label>性别:</label>
        <label>
          <input
            type="radio"
            name="gender"
            value="male"
            checked={formData.gender === 'male'}
            onChange={handleInputChange}
          />
          男
        </label>
        <label>
          <input
            type="radio"
            name="gender"
            value="female"
            checked={formData.gender === 'female'}
            onChange={handleInputChange}
          />
          女
        </label>
      </div>

      <div className="mb-3">
        <label>国家:</label>
        <select
          name="country"
          value={formData.country}
          onChange={handleInputChange}
        >
          <option value="china">中国</option>
          <option value="usa">美国</option>
          <option value="japan">日本</option>
          <option value="uk">英国</option>
        </select>
      </div>

      <button type="submit">提交</button>
      <button type="button" onClick={handleReset}>重置</button>
    </form>
  );
}

合成事件(SyntheticEvent)

React的事件系统使用合成事件,它是跨浏览器的事件包装器,具有与原生事件相同的接口。

原生事件
React包装
合成事件
事件处理器
function SyntheticEventDemo() {
  const handleEvent = (e) => {
    // 合成事件属性
    console.log('合成事件类型:', e.type);
    console.log('原生事件:', e.nativeEvent);

    // 合成事件会被重用,不能异步访问
    // 错误做法:
    // setTimeout(() => {
    //   console.log(e.type); // 可能为null
    // }, 100);

    // 正确做法:持久化事件
    e.persist();
    setTimeout(() => {
      console.log('异步访问事件类型:', e.type);
    }, 100);
  };

  return (
    <button onClick={handleEvent}>
      测试合成事件
    </button>
  );
}
重要: 合成事件会被池化(pooled)以提升性能。这意味着事件对象在事件回调后被重用,其属性会被置空。如果需要异步访问事件属性,必须调用 event.persist()

事件冒泡与捕获

function EventBubblingDemo() {
  const handleParentClick = () => {
    console.log('父元素被点击');
  };

  const handleChildClick = (e) => {
    console.log('子元素被点击');
    // 阻止事件冒泡
    e.stopPropagation();
  };

  const handleGrandchildClick = (e) => {
    console.log('孙元素被点击');
  };

  return (
    <div
      className="parent"
      onClick={handleParentClick}
      style={{ padding: '20px', background: '#f0f0f0' }}
    >
      父元素
      <div
        className="child"
        onClick={handleChildClick}
        style={{ padding: '20px', background: '#e0e0e0', margin: '10px' }}
      >
        子元素
        <div
          className="grandchild"
          onClick={handleGrandchildClick}
          style={{ padding: '20px', background: '#d0d0d0', margin: '10px' }}
        >
          孙元素
        </div>
      </div>
    </div>
  );
}

捕获阶段事件处理

function EventCaptureDemo() {
  const handleCapture = () => {
    console.log('捕获阶段: 父元素');
  };

  const handleBubble = () => {
    console.log('冒泡阶段: 父元素');
  };

  const handleChildClick = () => {
    console.log('子元素被点击');
  };

  return (
    <div
      onClickCapture={handleCapture}  // 捕获阶段
      onClick={handleBubble}          // 冒泡阶段
      style={{ padding: '20px', background: '#f0f0f0' }}
    >
      父元素(同时处理捕获和冒泡)
      <button onClick={handleChildClick}>
        点击我
      </button>
    </div>
  );
}

自定义事件与事件总线

// 简单的事件总线实现
class EventBus {
  constructor() {
    this.events = {};
  }

  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  }

  off(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);
    });
  }
}

// 创建全局事件总线实例
const eventBus = new EventBus();

// 组件A:发布事件
function ComponentA() {
  const handleClick = () => {
    eventBus.emit('customEvent', {
      message: '来自ComponentA的消息',
      timestamp: new Date().toISOString()
    });
  };

  return <button onClick={handleClick}>发送事件</button>;
}

// 组件B:订阅事件
function ComponentB() {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const handleCustomEvent = (data) => {
      setMessage(data.message);
    };

    eventBus.on('customEvent', handleCustomEvent);

    // 清理订阅
    return () => {
      eventBus.off('customEvent', handleCustomEvent);
    };
  }, []);

  return <div>接收到的消息: {message}</div>;
}

性能优化

import React, { useCallback, memo } from 'react';

// 1. 使用useCallback缓存事件处理器
function OptimizedComponent() {
  const [count, setCount] = useState(0);

  // 使用useCallback避免每次渲染都创建新函数
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []); // 依赖数组为空,函数只创建一次

  return (
    <div>
      <p>计数: {count}</p>
      <MemoizedButton onClick={handleClick} />
    </div>
  );
}

// 2. 使用memo避免不必要的重新渲染
const MemoizedButton = memo(function MemoizedButton({ onClick }) {
  console.log('MemoizedButton 渲染');
  return <button onClick={onClick}>点击我</button>;
});

// 3. 事件委托(适合大量子元素)
function EventDelegationDemo() {
  const handleListClick = useCallback((event) => {
    // 使用事件委托
    if (event.target.tagName === 'BUTTON') {
      const itemId = event.target.dataset.id;
      console.log(`点击了项目 ${itemId}`);
    }
  }, []);

  const items = Array.from({ length: 100 }, (_, i) => ({
    id: i + 1,
    name: `项目 ${i + 1}`
  }));

  return (
    <div onClick={handleListClick}>
      {items.map(item => (
        <div key={item.id} className="item">
          <span>{item.name}</span>
          <button data-id={item.id}>选择</button>
        </div>
      ))}
    </div>
  );
}
最佳实践总结:
  1. 使用useCallback缓存事件处理器
  2. 对于纯展示组件使用memo
  3. 避免在渲染方法中创建内联函数
  4. 使用事件委托处理大量相似元素
  5. 及时清理事件监听器(在useEffect的返回函数中)
  6. 使用event.persist()异步访问合成事件

常见错误与解决方法

// 错误1:忘记调用事件处理器
function WrongExample1() {
  const handleClick = () => {
    console.log('被点击了');
  };

  return (
    <div>
      {/* 错误:handleClick 后面缺少 () 实际上不会执行 */}
      <button onClick={handleClick}>正确</button>
      {/* 错误:这样会立即执行,而不是点击时执行 */}
      <button onClick={handleClick()}>错误</button>
    </div>
  );
}

// 错误2:异步访问合成事件
function WrongExample2() {
  const handleClick = (e) => {
    // 错误:异步访问事件属性
    setTimeout(() => {
      console.log(e.type); // 可能为null或undefined
    }, 100);

    // 正确:持久化事件
    // e.persist();
    // setTimeout(() => {
    //   console.log(e.type);
    // }, 100);
  };

  return <button onClick={handleClick}>测试</button>;
}

// 错误3:在类组件中忘记绑定this
class WrongExample3 extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };

    // 需要在构造函数中绑定this
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 如果没有绑定this,这里this会是undefined
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <p>计数: {this.state.count}</p>
        {/* 或者使用箭头函数 */}
        <button onClick={() => this.handleClick()}>
          点击我
        </button>
      </div>
    );
  }
}