事件处理

响应⽤户的交互操作,如点击、输⼊、提交等

什么是事件处理?

事件处理函数是响应用户操作的函数,如点击按钮、在输入框中输入文字、提交表单等。在 React 中,事件处理通过 JSX 属性来定义。

TypeScript
function Button() {
  const handleClick = () => {
    console.log('按钮被点击了!');
  };

  return (
    <button onClick={handleClick}>
      点击我
    </button>
  );
}
核心概念

React 事件使用驼峰命名(camelCase),而不是小写。 事件处理函数作为 props 传递,而不是字符串。

添加事件处理函数

定义处理函数

TypeScript
function Button() {
  // 在组件内部定义
  function handleClick() {
    console.log('点击!');
  }

  return <button onClick={handleClick}>点击</button>;
}

内联处理函数

TypeScript
function Button() {
  return (
    <button onClick={() => console.log('点击!')}>
      点击
    </button>
  );
}

直接传递函数

TypeScript
function Button() {
  return (
    <button onClick={function() { console.log('点击!'); }}>
      点击
    </button>
  );
}
常见错误

不要在 JSX 中调用函数:

TypeScript
// ❌ 错误:立即执行函数
<button onClick={handleClick()}>

// ✅ 正确:传递函数引用
<button onClick={handleClick}>

事件对象

访问事件对象

TypeScript
function Button() {
  const handleClick = (e) => {
    e.preventDefault();
    console.log('事件对象:', e);
    console.log('目标元素:', e.target);
    console.log('当前元素:', e.currentTarget);
  };

  return <button onClick={handleClick}>点击</button>;
}

常用的事件属性

TypeScript
function Form() {
  const handleSubmit = (e) => {
    // 阻止默认行为(如表单提交)
    e.preventDefault();

    // 阻止事件冒泡
    e.stopPropagation();

    // 获取事件目标
    const target = e.target;

    // 获取目标值(输入框)
    const value = target.value;

    console.log('表单提交:', value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="username" />
      <button type="submit">提交</button>
    </form>
  );
}

TypeScript 类型

TypeScript
import { MouseEvent, ChangeEvent, FormEvent } from 'react';

function Button() {
  const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
    console.log(e.currentTarget);
  };

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" onChange={handleChange} />
      <button onClick={handleClick}>点击</button>
    </form>
  );
}

传递参数

传递额外参数

TypeScript
function ButtonList() {
  const buttons = [
    { id: 1, label: '按钮 1' },
    { id: 2, label: '按钮 2' },
    { id: 3, label: '按钮 3' },
  ];

  const handleClick = (buttonId) => {
    console.log('按钮 ID:', buttonId);
  };

  return (
    <div>
      {buttons.map(button => (
        <button
          key={button.id}
          onClick={() => handleClick(button.id)}
        >
          {button.label}
        </button>
      ))}
    </div>
  );
}

同时传递事件和参数

TypeScript
function ButtonList() {
  const handleClick = (e, buttonId) => {
    e.preventDefault();
    console.log('按钮 ID:', buttonId);
    console.log('事件对象:', e);
  };

  return (
    <div>
      {buttons.map(button => (
        <button
          key={button.id}
          onClick={(e) => handleClick(e, button.id)}
        >
          {button.label}
        </button>
      ))}
    </div>
  );
}

使用 bind

TypeScript
function ButtonList() {
  const handleClick = function(buttonId, e) {
    e.preventDefault();
    console.log('按钮 ID:', buttonId);
  };

  return (
    <div>
      {buttons.map(button => (
        <button
          key={button.id}
          onClick={handleClick.bind(null, button.id)}
        >
          {button.label}
        </button>
      ))}
    </div>
  );
}
推荐方式

使用箭头函数是最常见和推荐的方式,因为它更简洁易读。 只在高性能要求的场景下考虑其他方式。

常见事件类型

鼠标事件

TypeScript
function MouseEvents() {
  return (
    <div
      onClick={() => console.log('单击')}
      onDoubleClick={() => console.log('双击')}
      onMouseEnter={() => console.log('鼠标进入')}
      onMouseLeave={() => console.log('鼠标离开')}
      onMouseMove={() => console.log('鼠标移动')}
      onMouseDown={() => console.log('鼠标按下')}
      onMouseUp={() => console.log('鼠标释放')}
      style={{ padding: '20px', border: '1px solid black' }}
    >
      鼠标事件区域
    </div>
  );
}

键盘事件

TypeScript
function KeyboardEvents() {
  const handleKeyDown = (e) => {
    console.log('按键:', e.key);
    console.log('代码:', e.code);

    // 检测特定键
    if (e.key === 'Enter') {
      console.log('回车键被按下');
    }

    // 检测组合键
    if (e.ctrlKey && e.key === 's') {
      e.preventDefault();
      console.log('Ctrl+S 被按下');
    }
  };

  return (
    <input
      type="text"
      onKeyDown={handleKeyDown}
      onKeyUp={(e) => console.log('按键释放:', e.key)}
      onKeyPress={(e) => console.log('按键字符:', e.key)}
      placeholder="输入文字..."
    />
  );
}

表单事件

TypeScript
function FormEvents() {
  const [value, setValue] = useState('');

  const handleChange = (e) => {
    setValue(e.target.value);
  };

  const handleFocus = () => {
    console.log('输入框获得焦点');
  };

  const handleBlur = () => {
    console.log('输入框失去焦点');
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('表单提交:', value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={value}
        onChange={handleChange}
        onFocus={handleFocus}
        onBlur={handleBlur}
      />
      <button type="submit">提交</button>
    </form>
  );
}

UI 事件

TypeScript
function UIEvents() {
  const [isVisible, setIsVisible] = useState(true);

  return (
    <div
      onScroll={() => console.log('滚动事件')}
      style={{ height: '200px', overflow: 'auto' }}
    >
      <p>滚动此区域...</p>
      <div style={{ height: '1000px' }}>
        很长的内容
      </div>

      {isVisible && (
        <div
          onTransitionEnd={() => console.log('动画结束')}
          onAnimationEnd={() => console.log('动画完成')}
        >
          动画内容
        </div>
      )}
    </div>
  );
}
事件参考

React 支持所有标准浏览器事件。完整列表请参考:

React 事件参考

合成事件 (SyntheticEvent)

什么是合成事件?

React 使用 SyntheticEvent 来包装浏览器原生事件,提供跨浏览器的一致性 API。

TypeScript
function Button() {
  const handleClick = (e) => {
    console.log('是合成事件:', e instanceof SyntheticEvent);
    console.log('原生事件:', e.nativeEvent);

    // 阻止默认行为
    e.preventDefault();

    // 阻止冒泡
    e.stopPropagation();
  };

  return <button onClick={handleClick}>点击</button>;
}

事件池

在 React 17 之前,事件对象会被池化以提升性能。React 17+ 不再使用事件池,所以可以安全地异步访问事件对象:

TypeScript
function Button() {
  const handleClick = (e) => {
    // ✅ React 17+:可以安全地异步访问
    setTimeout(() => {
      console.log(e.type);
    }, 1000);
  };

  return <button onClick={handleClick}>点击</button>;
}

访问原生事件

TypeScript
function Button() {
  const handleClick = (e) => {
    // 访问原生 DOM 事件
    const nativeEvent = e.nativeEvent;
    console.log('原生事件:', nativeEvent);

    // 访问原生 DOM 元素
    const domNode = e.target;
    console.log('DOM 节点:', domNode);
  };

  return <button onClick={handleClick}>点击</button>;
}

事件传播

事件冒泡

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

  const handleChildClick = (e) => {
    console.log('子元素被点击');
    // 不阻止冒泡,事件会继续传播到父元素
  };

  return (
    <div onClick={handleParentClick} style={{ padding: '20px', background: 'lightblue' }}>
      <button onClick={handleChildClick}>
        点击我(会触发两个处理函数)
      </button>
    </div>
  );
}

停止冒泡

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

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

  return (
    <div onClick={handleParentClick} style={{ padding: '20px', background: 'lightblue' }}>
      <button onClick={handleChildClick}>
        点击我(只触发子元素处理函数)
      </button>
    </div>
  );
}

阻止默认行为

TypeScript
function PreventDefault() {
  const handleClick = (e) => {
    e.preventDefault(); // 阻止链接跳转
    console.log('链接被点击,但不会跳转');
  };

  const handleSubmit = (e) => {
    e.preventDefault(); // 阻止表单提交
    console.log('表单提交被阻止');
  };

  return (
    <div>
      <a href="https://example.com" onClick={handleClick}>
        点击链接(不会跳转)
      </a>

      <form onSubmit={handleSubmit}>
        <input type="text" />
        <button type="submit">提交</button>
      </form>
    </div>
  );
}
注意

preventDefault()stopPropagation() 只影响当前事件。 如果同一元素上有多个处理函数,其他处理函数仍会执行。

受控组件

什么是受控组件?

受控组件是指其值由 React state 控制的表单元素。这是 React 中处理表单的推荐方式。

TypeScript
function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('提交:', { name, email });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>
          姓名:
          <input
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
        </label>
      </div>

      <div>
        <label>
          邮箱:
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </label>
      </div>

      <button type="submit">提交</button>
    </form>
  );
}

文本输入

TypeScript
function TextInput() {
  const [text, setText] = useState('');

  return (
    <input
      type="text"
      value={text}
      onChange={(e) => setText(e.target.value)}
      placeholder="输入文本..."
    />
  );
}

文本区域

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

  return (
    <textarea
      value={message}
      onChange={(e) => setMessage(e.target.value)}
      placeholder="输入消息..."
      rows={5}
    />
  );
}

选择框

TypeScript
function SelectBox() {
  const [fruit, setFruit] = useState('apple');

  return (
    <select value={fruit} onChange={(e) => setFruit(e.target.value)}>
      <option value="apple">苹果</option>
      <option value="banana">香蕉</option>
      <option value="orange">橙子</option>
    </select>
  );
}

复选框

TypeScript
function Checkbox() {
  const [isChecked, setIsChecked] = useState(false);

  return (
    <label>
      <input
        type="checkbox"
        checked={isChecked}
        onChange={(e) => setIsChecked(e.target.checked)}
      />
      同意条款
    </label>
  );
}

单选按钮

TypeScript
function RadioButton() {
  const [gender, setGender] = useState('male');

  return (
    <div>
      <label>
        <input
          type="radio"
          name="gender"
          value="male"
          checked={gender === 'male'}
          onChange={(e) => setGender(e.target.value)}
        />
        男
      </label>

      <label>
        <input
          type="radio"
          name="gender"
          value="female"
          checked={gender === 'female'}
          onChange={(e) => setGender(e.target.value)}
        />
        女
      </label>
    </div>
  );
}
受控 vs 非受控

受控组件:值由 React state 控制(推荐)

非受控组件:值由 DOM 自身管理(使用 ref)

受控组件更容易实现表单验证、条件禁用等功能。

事件监听器

useEffect 添加监听器

TypeScript
import { useEffect, useState } from 'react';

function WindowSize() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    // 清理:移除监听器
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return <div>窗口宽度:{width}px</div>;
}

键盘快捷键

TypeScript
function KeyboardShortcuts() {
  useEffect(() => {
    const handleKeyDown = (e) => {
      // Ctrl/Cmd + S 保存
      if ((e.ctrlKey || e.metaKey) && e.key === 's') {
        e.preventDefault();
        console.log('保存文档');
      }

      // Escape 取消
      if (e.key === 'Escape') {
        console.log('取消操作');
      }
    };

    window.addEventListener('keydown', handleKeyDown);

    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, []);

  return <div>按 Ctrl+S 保存</div>;
}
重要

务必在 useEffect 返回的清理函数中移除事件监听器, 否则会导致内存泄漏。

常见模式

表单验证

TypeScript
function ValidatedForm() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const validateEmail = (email) => {
    const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return re.test(email);
  };

  const handleChange = (e) => {
    const value = e.target.value;
    setEmail(value);

    if (value && !validateEmail(value)) {
      setError('请输入有效的邮箱地址');
    } else {
      setError('');
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (error) {
      console.log('表单有错误');
      return;
    }
    console.log('提交:', email);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={handleChange}
      />
      {error && <div style={{ color: 'red' }}>{error}</div>}
      <button type="submit">提交</button>
    </form>
  );
}

防抖输入

TypeScript
import { useState, useEffect } from 'react';

function DebouncedInput() {
  const [value, setValue] = useState('');
  const [debouncedValue, setDebouncedValue] = useState('');

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, 500);

    return () => {
      clearTimeout(timer);
    };
  }, [value]);

  return (
    <div>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="输入文字..."
      />
      <p>防抖后的值:{debouncedValue}</p>
    </div>
  );
}

确认操作

TypeScript
function ConfirmButton() {
  const [isConfirmed, setIsConfirmed] = useState(false);

  const handleClick = () => {
    if (!isConfirmed) {
      setIsConfirmed(true);
      // 3秒后重置
      setTimeout(() => setIsConfirmed(false), 3000);
      return;
    }

    console.log('操作已确认');
    setIsConfirmed(false);
  };

  return (
    <button onClick={handleClick}>
      {isConfirmed ? '确认删除?' : '删除'}
    </button>
  );
}

拖拽

TypeScript
function DraggableBox() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [isDragging, setIsDragging] = useState(false);

  const handleMouseDown = (e) => {
    setIsDragging(true);
  };

  useEffect(() => {
    const handleMouseMove = (e) => {
      if (isDragging) {
        setPosition({
          x: e.clientX,
          y: e.clientY
        });
      }
    };

    const handleMouseUp = () => {
      setIsDragging(false);
    };

    if (isDragging) {
      window.addEventListener('mousemove', handleMouseMove);
      window.addEventListener('mouseup', handleMouseUp);
    }

    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
      window.removeEventListener('mouseup', handleMouseUp);
    };
  }, [isDragging]);

  return (
    <div
      onMouseDown={handleMouseDown}
      style={{
        position: 'absolute',
        left: position.x,
        top: position.y,
        width: '100px',
        height: '100px',
        background: 'lightblue',
        cursor: isDragging ? 'grabbing' : 'grab'
      }}
    >
      拖拽我
    </div>
  );
}

最佳实践

1. 使用语义化的事件名称

TypeScript
// ✅ 好:语义化的名称
const handleSubmit = () => { };
const handleClick = () => { };
const handleChange = () => { };

// ❌ 差:不清晰的名称
const doIt = () => { };
const handleEvent = () => { };

2. 提取处理逻辑

TypeScript
// ✅ 好:逻辑分离
const validateForm = () => { };
const submitData = () => { };
const handleSubmit = (e) => {
  e.preventDefault();
  if (validateForm()) {
    submitData();
  }
};

// ❌ 差:所有逻辑在一起
const handleSubmit = (e) => {
  e.preventDefault();
  // 大量逻辑...
};

3. 避免在渲染中创建函数

TypeScript
// ✅ 好:使用 useCallback
const handleClick = useCallback(() => {
  doSomething(dependency);
}, [dependency]);

// ❌ 差:每次渲染都创建新函数
<button onClick={() => doSomething(dependency)}>

4. 清理事件监听器

TypeScript
useEffect(() => {
  const handleResize = () => { };
  window.addEventListener('resize', handleResize);

  // ✅ 总是清理
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

5. 使用 TypeScript 类型

TypeScript
const handleClick = (e: MouseEvent<HTMLButtonElement>) => { };
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { };
const handleSubmit = (e: FormEvent<HTMLFormElement>) => { };

常见问题

事件没有触发?

确保你传递的是函数引用,而不是函数调用结果:

TypeScript
// ❌ 错误:立即执行
<button onClick={handleClick()}>

// ✅ 正确:传递函数
<button onClick={handleClick}>

如何获取输入框的值?

TypeScript
const handleChange = (e) => {
  const value = e.target.value;
  setValue(value);
};

如何阻止表单提交?

TypeScript
const handleSubmit = (e) => {
  e.preventDefault();
};
这篇文章有帮助吗?