一句话定义
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)。
自测题
- 为什么
onClick={handleClick(id)}会导致无限循环(若 handleClick 内含 setState)? - 阻止
<a href>跳转应该写什么?return false为什么不行? - React 的
onChange对应原生什么事件?为什么这样设计? - 一个父 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>