讲解
React 里绑事件是「传函数」而不是「写字符串」:<button onClick={handleClick}>。注意传的是函数引用,onClick={handleClick()} 是立刻调用并把返回值当回调——每次渲染都执行一遍,新手最高频的 bug 没有之一。需要传参就包一层箭头函数:onClick={() => handleDelete(id)}。
事件回调拿到的 e 是 React 的合成事件(SyntheticEvent):它抹平了浏览器差异,接口和原生事件一致(e.target、e.preventDefault()、e.stopPropagation())。React 18 起事件委托到根容器统一监听,性能开销极小,你只管写业务逻辑。
表单类事件里 onChange 最常用:React 的 onChange 等价于原生 input 事件,每次输入都触发,配合状态就是受控组件(表单专章展开)。常见约定:处理函数以 handle 开头(handleSubmit、handleChange),作为 props 传下去时以 on 开头(onSubmit、onChange)——读代码时一眼分清「定义者」和「使用者」。
事件传播在 React 里照常工作:子元素点击会冒泡到父元素的 onClick。需要阻断时 e.stopPropagation(),比如弹窗「点遮罩关闭、点内容不关闭」。还有一个细节:事件回调里读状态,读到的是「触发那次渲染时」的快照——异步回调里状态可能已经变了,这类场景用 ref 或函数式更新解决(后面章节展开)。
示例
验证「传引用 vs 传调用结果」的区别,以及冒泡/阻止传播(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
const log = [];
// onClick={handleClick} —— 传引用:只在点击时执行
function handleClick() {
log.push('点击了');
}
const onClickCorrect = handleClick; // 渲染时不执行
assert.deepStrictEqual(log, []);
onClickCorrect(); // 模拟点击
assert.deepStrictEqual(log, ['点击了']);
// onClick={handleClick()} —— 传调用结果:渲染时就执行了
const onClickBuggy = handleClick(); // 立即执行!
assert.deepStrictEqual(log, ['点击了', '点击了']);
assert.strictEqual(onClickBuggy, undefined); // onClick 拿到的是 undefined
// 冒泡:子元素事件触发父元素处理器
function createEvent(target) {
return {
target,
stopped: false,
stopPropagation() {
this.stopped = true;
},
};
}
function dispatch(e, handlers) {
for (const h of handlers) {
if (e.stopped) break; // 被 stopPropagation 切断
h(e);
}
}
const fired = [];
const e1 = createEvent('content');
dispatch(e1, [() => fired.push('内容'), () => fired.push('遮罩')]);
assert.deepStrictEqual(fired, ['内容', '遮罩']); // 冒泡到遮罩
const e2 = createEvent('content');
const fired2 = [];
dispatch(e2, [(e) => { fired2.push('内容'); e.stopPropagation(); }, () => fired2.push('遮罩')]);
assert.deepStrictEqual(fired2, ['内容']); // 遮罩没收到
console.log('传引用:点击才执行;传调用:渲染就执行且回调是 undefined');
console.log('冒泡默认触发父级;stopPropagation 后只剩:', fired2.join('、'));
真实组件:
function TodoItem({ todo, onDelete }) {
return (
<li>
{todo.text}
{/* 传参:包一层箭头函数 */}
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
);
}
function Modal({ onClose, children }) {
return (
<div className="mask" onClick={onClose}>
{/* 点内容不关闭:阻止冒泡到遮罩 */}
<div className="body" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
);
}
常见坑
- onClick={fn()} 立刻执行:要传函数本身或箭头函数包装,这是新手第一大坑。
- 在渲染期间发请求/改状态:事件回调才是副作用的家,渲染函数必须纯净。
- 异步回调里读旧状态:setTimeout 里的 state 是闭包快照,需要最新值用 ref 或函数式 setState。
- event.persist() 老代码:React 17 起合成事件不再池化,persist 已无用,看到老代码可以删。
- native 事件混用忘记清理:useEffect 里 addEventListener 必须配 removeEventListener(effect 章节细讲)。
小结
事件传函数引用,传参包箭头函数;合成事件接口同原生;冒泡可 stopPropagation;handle/on 命名约定分清定义与使用。下一章:让界面「记住事情」——useState。