React 的事件处理机制与原生 DOM 事件有相似之处,但包裹了一层合成事件(SyntheticEvent),抹平了浏览器之间的差异,并统一了事件对象的行为。掌握 React 中的事件绑定、参数传递和事件对象用法,是编写交互逻辑的基础。
事件绑定:用 JSX 属性直接监听
React 使用驼峰式属性名绑定事件,比如 onClick、onChange、onSubmit 等。事件处理函数通常以 handle 开头,这是社区约定。
function Button() {
function handleClick() {
console.log('按钮被点击了');
}
return <button onClick={handleClick}>点击我</button>;
}
关键规则:
- 传入的是函数引用(
handleClick),而不是函数调用(handleClick())。如果写成onClick={handleClick()},函数会在渲染时立即执行,这通常不是你想要的行为。 - 事件处理函数可以定义为组件内部的普通函数,也可以使用箭头函数。使用箭头函数的好处是可以自动绑定当前词法作用域中的变量(避免
this困扰,但在函数组件中无需考虑this)。
传递参数:箭头函数或柯里化
实际开发中经常需要在触发事件时传递额外参数,比如列表某一项的 id。有三种常用方式:
方式一:内联箭头函数(最常用)
function ItemList({ items }) {
function handleDelete(id) {
console.log('删除项目:', id);
}
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<button onClick={() => handleDelete(item.id)}>删除</button>
</li>
))}
</ul>
);
}
箭头函数 () => handleDelete(item.id) 创建了一个新函数,当点击时它才去调用 handleDelete 并传入 item.id。这是最直观的方式,但每次渲染都会生成新函数,如果 item 数量很大且无需优化,一般性能影响可忽略。若出现性能瓶颈,可结合 useCallback 或通过数据属性优化。
方式二:使用 bind 绑定参数
<button onClick={handleDelete.bind(null, item.id)}>删除</button>
bind 会返回一个新的函数,预设了第一个参数。但可读性不如箭头函数,社区较少采用。
*方式三:通过自定义属性 data- 间接传参**
function handleDelete(e) {
const id = e.currentTarget.dataset.id;
console.log('删除项目:', id);
}
<button data-id={item.id} onClick={handleDelete}>删除</button>
这种方式避免了每次渲染生成新函数,适用于对性能敏感的极大量列表(但多数情况直接使用箭头函数足够清晰)。事件处理函数通过 e.currentTarget.dataset 读取参数。
事件对象:合成事件与原生事件
React 传递给事件处理函数的第一个参数是 SyntheticEvent(合成事件),它是对浏览器原生事件的跨浏览器封装,提供了统一的接口。
function Input() {
function handleChange(e) {
// e 是 SyntheticEvent
console.log(e.target.value); // 触发事件的元素
console.log(e.type); // 事件类型,如 "change"
}
return <input onChange={handleChange} />;
}
常用属性和方法:
e.target:触发事件的 DOM 元素(比如用户实际点击的按钮)。e.currentTarget:事件绑定的元素(事件处理函数所附加的元素)。在 React 中由于事件委托,e.currentTarget通常与e.target一致。e.type:事件类型(如click、change)。e.preventDefault():阻止默认行为(如表单提交刷新页面)。e.stopPropagation():阻止事件冒泡。
重要:React 事件的异步访问与持久化
React 17 之前,合成事件对象在事件处理函数执行完毕后会被回收(出于性能考虑),所以无法在异步函数(如 setTimeout、Promise)中直接访问事件对象。React 17 之后这一行为已变更,不再需要手动调用 e.persist(),合成事件不会立即置空。但在旧项目中仍可能遇到旧行为,了解即可。
// React 16 及之前,异步访问需要调用 e.persist()
function handleClick(e) {
e.persist(); // 保留事件对象
setTimeout(() => {
console.log(e.target); // 否则这里会是 null
}, 1000);
}
// React 17+ 不再需要 e.persist()
阻止默认行为与冒泡
在表单提交、链接跳转等场景,需要阻止浏览器的默认行为。使用 e.preventDefault() 即可。
function Form() {
function handleSubmit(e) {
e.preventDefault(); // 阻止页面刷新
const formData = new FormData(e.currentTarget);
console.log(Object.fromEntries(formData));
}
return (
<form onSubmit={handleSubmit}>
<input name="username" />
<button type="submit">提交</button>
</form>
);
}
有时需要阻止事件冒泡到父元素,使用 e.stopPropagation()。注意区分:
e.stopPropagation()阻止事件向上冒泡,但不阻止当前元素的其他处理函数。e.preventDefault()阻止默认行为,但事件仍会冒泡。
常见事件类型速查
| 事件名 | 典型元素 | 说明 |
|--------|---------|------|
| onClick | 任何元素 | 鼠标单击 |
| onChange | <input>、<select>、<textarea> | 值改变时触发(React 中表现接近原生 input 事件) |
| onSubmit | <form> | 表单提交 |
| onFocus / onBlur | 可聚焦元素 | 获得/失去焦点 |
| onKeyDown / onKeyUp / onKeyPress | 可聚焦元素 | 键盘事件 |
| onMouseEnter / onMouseLeave | 任何元素 | 鼠标进出(不冒泡) |
| onScroll | 滚动容器 | 滚动事件 |
实际开发中的注意事项
- 避免在 JSX 中使用内联函数时造成不必要的重渲染:当组件比较庞大且
onClick={() => handler(param)}作为 Props 传给子组件时,可能会破坏子组件的React.memo优化,因为每次父组件渲染都会生成新的函数引用。此时可以使用useCallback或者子组件配合data-*属性来接收参数。
- 在
onChange中更新状态要记得受控:如果输入框的value绑定了状态,则必须提供onChange来更新状态,否则输入框将无法输入。
- 事件处理函数内部可以访问最新 state 吗? 由于闭包,如果你在事件处理函数中使用了 state 但没有正确处理依赖,可能会读到过时的值。在函数组件中,只要状态更新不是被异步打乱顺序,通常事件触发时 state 已是最新(因为事件处理函数是在渲染时注册的最新版本)。遇到闭包陷阱时,可以使用函数式更新
setState(prev => ...)来基于最新状态计算。
React 的事件系统让跨浏览器兼容变得透明,但也需要你理解其合成事件的特性和参数传递的恰当方式,才能写出稳健的交互逻辑。