React 学习站
React›基础›入门

事件处理

入门基础

一句话定义

React 事件是 props 上的 onXxx 回调:<button onClick={handleClick}>;React 在根部用统一的事件系统(SyntheticEvent,基于原生事件委托)分发,你拿到的 e 是跨浏览器一致的包装对象。

为什么重要

交互 = 状态 + 事件。所有「用户意图改变状态」的路径都从这里进入 React。同时这里有两个高频坑:函数引用 vs 函数调用、事件对象的 pooling/异步访问(后者在现代版本已缓和,但历史代码里常见)。

前置知识

  • kp-005 setState。
  • 原生 addEventListener 的基本概念(知道「委托」这个词即可)。

核心概念

  • 驼峰事件名:onClick、onChange、onSubmit、onKeyDown。
  • 传函数不传调用:onClick={fn},需要参数时 onClick={() => fn(id)}。
  • preventDefault 替代 return false:e.preventDefault();return false 在 JSX 里无效。
  • stopPropagation:阻止事件冒泡到父级处理器。
  • 合成事件(SyntheticEvent):统一包装层,兼容各浏览器差异。

原理 / 机制

React 17+ 的事件委托挂在根容器上(历史上挂在 document):你在叶子按钮上写 onClick,真实监听器在根部,捕获阶段定位目标后按冒泡路径调用沿途处理器。好处是统一时序、一致的行为、更少的真实监听器。

参数传递的三种写法对比:

jsx<button onClick={handleClick} />              // 1. 直接传引用
<button onClick={() => handleClick(id)} />     // 2. 箭头包装传参
<button onClick={(e) => { e.stopPropagation(); handleClick(id); }} /> // 3. 需要事件对象

写法 2 每次渲染都创建新函数——普通场景无所谓;若该 prop 传给了 memo 组件,会击穿记忆化(kp-018 的主题)。

表单提交的标准形状:

jsx<form onSubmit={e => {
  e.preventDefault();          // 阻止整页刷新
  doSubmit(values);
}}>
  <input value={v} onChange={e => setV(e.target.value)} />
  <button type="submit">提交</button>
</form>

onChange 在 React 里语义是「每次输入变化」(对应原生 input 事件),不是原生 change(失焦才触发)——最常见的跨框架混淆点。

直观类比

原生事件像你家门口装对讲机(每户自己监听);React 合成事件像小区总台统一接警再分派:门铃都接到总台(根节点委托),总台按楼栋楼层(组件树路径)叫人。你要「别惊动楼上」就是 stopPropagation,要「别让访客进大门」就是 preventDefault。

实例 / 案例

列表项删除 + 键盘可访问:

jsxfunction TodoItem({ todo, onDelete }) {
  function handleKey(e) {
    if (e.key === 'Delete') onDelete(todo.id);
  }
  return (
    <li tabIndex={0} onKeyDown={handleKey}>
      {todo.title}
      <button onClick={() => onDelete(todo.id)} aria-label={`删除 ${todo.title}`}>
        ×
      </button>
    </li>
  );
}

要点:删除逻辑写在子组件事件里调用 onDelete(意图上行,kp-004);键盘与鼠标两条路径都可触发;图标按钮配 aria-label。

常见误区

  • onClick={setOpen(true)}:渲染时立即执行 setState → 无限循环;必须包箭头函数。
  • 以为 return false 能阻止默认行为:必须 e.preventDefault()。
  • onChange 语义错位:来自 Vue/原生经验的人以为「失焦才触发」;React 里每敲一个字符就触发。
  • 异步代码里过度担心 e:React 17+ 已移除事件池(pooling),setTimeout(() => e.target, 0) 可用;但老项目代码里常见 e.persist() 的历史包袱,阅读时知道即可。
  • 在事件处理器里直接操作 DOM 样式:应改状态,让渲染结果表达样式(kp-001)。

自测题

  1. 为什么 onClick={handleClick(id)} 会导致无限循环(若 handleClick 内含 setState)?
  2. 阻止 <a href> 跳转应该写什么?return false 为什么不行?
  3. React 的 onChange 对应原生什么事件?为什么这样设计?
  4. 一个父 div 和子 button 都有 onClick,点击按钮时两者都会执行吗?如何只让子级生效?

与其他知识点的关系

  • 向前:事件是「改变状态」的主要入口,与 kp-005 配套。
  • 向后:kp-011 用 onChange 实现受控表单;kp-018 讨论回调引用稳定性;kp-025 的 form action 把「提交类事件」交给 React 管理,逐步取代手写 onSubmit。

延伸阅读

  • Responding to Events:<https://react.dev/learn/responding-to-events>
  • SyntheticEvent 参考旧文档:<https://legacy.reactjs.org/docs/events.html>